From db5208c58b74575a315f6deddeff09107cd95af0 Mon Sep 17 00:00:00 2001 From: AdheipSingh Date: Wed, 18 Dec 2024 22:52:31 +0530 Subject: [PATCH 01/13] refactor + sc prompts + installer.yaml --- cmd/installer.go | 239 +------------------- cmd/uninstaller.go | 13 +- pkg/installer/installer.go | 410 +++++++++++++++++++++++++++++------ pkg/installer/model.go | 15 +- pkg/installer/plans.go | 10 +- pkg/installer/spinner.go | 2 +- pkg/installer/uninstaller.go | 274 ++++++++++------------- 7 files changed, 484 insertions(+), 479 deletions(-) diff --git a/cmd/installer.go b/cmd/installer.go index a8c9116..9c86652 100644 --- a/cmd/installer.go +++ b/cmd/installer.go @@ -16,22 +16,8 @@ package cmd import ( - "encoding/base64" - "encoding/json" - "fmt" - "net" - "os" - "os/exec" - "runtime" - "strings" - "sync" - "time" - - "pb/pkg/common" - "pb/pkg/helm" "pb/pkg/installer" - "github.com/briandowns/spinner" "github.com/spf13/cobra" ) @@ -41,230 +27,9 @@ var InstallOssCmd = &cobra.Command{ Use: "oss", Short: "Deploy Parseable OSS", Example: "pb install oss", - RunE: func(cmd *cobra.Command, _ []string) error { + Run: func(cmd *cobra.Command, _ []string) { // Add verbose flag cmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Enable verbose logging") - - // Print the banner - printBanner() - - // Prompt user to select a deployment plan - selectedPlan, err := installer.PromptUserPlanSelection() - if err != nil { - return err - } - - fmt.Printf( - common.Cyan+" Ingestion Speed: %s\n"+ - common.Cyan+" Per Day Ingestion: %s\n"+ - common.Cyan+" Query Performance: %s\n"+ - common.Cyan+" CPU & Memory: %s\n"+ - common.Reset, selectedPlan.IngestionSpeed, selectedPlan.PerDayIngestion, - selectedPlan.QueryPerformance, selectedPlan.CPUAndMemorySpecs) - - // Get namespace and chart values from installer - valuesHolder, chartValues := installer.Installer(selectedPlan) - - // Helm application configuration - apps := []helm.Helm{ - { - ReleaseName: "parseable", - Namespace: valuesHolder.ParseableSecret.Namespace, - RepoName: "parseable", - RepoURL: "https://charts.parseable.com", - ChartName: "parseable", - Version: "1.6.5", - Values: chartValues, - }, - } - - // Create a spinner - spinner := createDeploymentSpinner(valuesHolder.ParseableSecret.Namespace) - - // 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() - - // Deploy using Helm - var wg sync.WaitGroup - errCh := make(chan error, len(apps)) - for _, app := range apps { - wg.Add(1) - go func(app helm.Helm) { - defer wg.Done() - if err := helm.Apply(app, verbose); err != nil { - errCh <- err - return - } - }(app) - } - - wg.Wait() - close(errCh) - - // Stop the spinner and restore stdout - spinner.Stop() - if !verbose { - os.Stdout = oldStdout - } - - // Check for errors - for err := range errCh { - if err != nil { - return err - } - } - - // Print success banner - printSuccessBanner(valuesHolder.ParseableSecret.Namespace, string(valuesHolder.DeploymentType), apps[0].Version, valuesHolder.ParseableSecret.Username, valuesHolder.ParseableSecret.Password) - - return nil + installer.Installer(verbose) }, } - -// printSuccessBanner remains the same as in the original code -func printSuccessBanner(namespace, deployment, version, username, password string) { - var ingestionURL, serviceName string - if deployment == "standalone" { - ingestionURL = "parseable." + namespace + ".svc.cluster.local" - serviceName = "parseable" - } else if deployment == "distributed" { - ingestionURL = "parseable-ingestor-svc." + namespace + ".svc.cluster.local" - serviceName = "parseable-query-svc" - } - - // Encode credentials to Base64 - credentials := map[string]string{ - "username": username, - "password": password, - } - credentialsJSON, err := json.Marshal(credentials) - if err != nil { - fmt.Printf("failed to marshal credentials: %v\n", err) - return - } - - base64EncodedString := base64.StdEncoding.EncodeToString(credentialsJSON) - - fmt.Println("\n" + common.Green + "šŸŽ‰ Parseable Deployment Successful! šŸŽ‰" + common.Reset) - fmt.Println(strings.Repeat("=", 50)) - - fmt.Printf("%s Deployment Details:\n", common.Blue+"ā„¹ļø ") - fmt.Printf(" • Namespace: %s\n", common.Blue+namespace) - fmt.Printf(" • Chart Version: %s\n", common.Blue+version) - fmt.Printf(" • Ingestion URL: %s\n", ingestionURL) - - fmt.Println("\n" + common.Blue + "šŸ”— Resources:" + common.Reset) - fmt.Println(common.Blue + " • Documentation: https://www.parseable.com/docs/server/introduction") - fmt.Println(common.Blue + " • Stream Management: https://www.parseable.com/docs/server/api") - - fmt.Println("\n" + common.Blue + "Happy Logging!" + common.Reset) - - // Port-forward the service - localPort := "8000" - fmt.Printf(common.Green+"Port-forwarding %s service on port %s...\n"+common.Reset, serviceName, localPort) - - if err = startPortForward(namespace, serviceName, "80", localPort); err != nil { - fmt.Printf(common.Red+"failed to port-forward service: %s", err.Error()) - } - - // Redirect to UI - localURL := fmt.Sprintf("http://localhost:%s/login?q=%s", localPort, base64EncodedString) - fmt.Printf(common.Green+"Opening Parseable UI at %s\n"+common.Reset, localURL) - openBrowser(localURL) -} - -func createDeploymentSpinner(namespace 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+"Deploying to %s ", namespace) - - return s -} - -// printBanner displays a welcome banner -func printBanner() { - banner := ` - -------------------------------------- - Welcome to Parseable OSS Installation - -------------------------------------- -` - fmt.Println(common.Green + banner + common.Reset) -} - -func startPortForward(namespace, serviceName, remotePort, localPort string) error { - // Build the port-forward command - cmd := exec.Command("kubectl", "port-forward", - fmt.Sprintf("svc/%s", serviceName), - fmt.Sprintf("%s:%s", localPort, remotePort), - "-n", namespace, - ) - - // Redirect the command's output to the standard output for debugging - if !verbose { - cmd.Stdout = nil // Suppress standard output - cmd.Stderr = nil // Suppress standard error - } else { - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - } - - // Run the command in the background - if err := cmd.Start(); err != nil { - return fmt.Errorf("failed to start port-forward: %w", err) - } - - // Run in a goroutine to keep it alive - go func() { - _ = cmd.Wait() - }() - - // Check connection on the forwarded port - retries := 10 - for i := 0; i < retries; i++ { - conn, err := net.Dial("tcp", fmt.Sprintf("localhost:%s", localPort)) - if err == nil { - conn.Close() // Connection successful, break out of the loop - fmt.Println(common.Green + "Port-forwarding successfully established!") - time.Sleep(5 * time.Second) // some delay - return nil - } - time.Sleep(3 * time.Second) // Wait before retrying - } - - // If we reach here, port-forwarding failed - cmd.Process.Kill() // Stop the kubectl process - return fmt.Errorf(common.Red+"failed to establish port-forward connection to localhost:%s", localPort) -} - -func openBrowser(url string) { - var cmd *exec.Cmd - switch os := runtime.GOOS; os { - case "windows": - cmd = exec.Command("rundll32", "url.dll,FileProtocolHandler", url) - case "darwin": - cmd = exec.Command("open", url) - case "linux": - cmd = exec.Command("xdg-open", url) - default: - fmt.Printf("Please open the following URL manually: %s\n", url) - return - } - cmd.Start() -} diff --git a/cmd/uninstaller.go b/cmd/uninstaller.go index 66b51e6..6d8ca39 100644 --- a/cmd/uninstaller.go +++ b/cmd/uninstaller.go @@ -16,11 +16,6 @@ package cmd import ( - "fmt" - - "pb/pkg/common" - "pb/pkg/installer" - "github.com/spf13/cobra" ) @@ -33,11 +28,11 @@ var UnInstallOssCmd = &cobra.Command{ cmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Enable verbose logging") // Print the banner - printBanner() + //printBanner() - if err := installer.Uninstaller(verbose); err != nil { - fmt.Println(common.Red + err.Error()) - } + // if err := installer.Uninstaller(verbose); err != nil { + // fmt.Println(common.Red + err.Error()) + // } return nil }, diff --git a/pkg/installer/installer.go b/pkg/installer/installer.go index dfc01d4..ca4ea52 100644 --- a/pkg/installer/installer.go +++ b/pkg/installer/installer.go @@ -20,29 +20,48 @@ import ( "bytes" "context" "encoding/base64" + "encoding/json" "fmt" "log" + "net" "os" + "os/exec" "path/filepath" + "runtime" "strings" + "sync" + "time" "pb/pkg/common" + "pb/pkg/helm" "github.com/manifoldco/promptui" - yamlv2 "gopkg.in/yaml.v2" + yamlv3 "gopkg.in/yaml.v3" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/yaml" "k8s.io/client-go/discovery" "k8s.io/client-go/dynamic" + "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" "k8s.io/client-go/restmapper" "k8s.io/client-go/tools/clientcmd" ) -// Installer orchestrates the installation process -func Installer(_ Plan) (values *ValuesHolder, chartValues []string) { +func Installer(verbose bool) { + printBanner() + waterFall(verbose) +} + +// waterFall orchestrates the installation process +func waterFall(verbose bool) { + var chartValues []string + _, err := promptUserPlanSelection() + if err != nil { + log.Fatalf("Failed to prompt for plan selection: %v", err) + } + if _, err := promptK8sContext(); err != nil { log.Fatalf("Failed to prompt for kubernetes context: %v", err) } @@ -51,13 +70,13 @@ func Installer(_ Plan) (values *ValuesHolder, chartValues []string) { chartValues = append(chartValues, "parseable.highAvailability.enabled=true") // Prompt for namespace and credentials - pbSecret, err := promptNamespaceAndCredentials() + pbInfo, err := promptNamespaceAndCredentials() if err != nil { log.Fatalf("Failed to prompt for namespace and credentials: %v", err) } // Prompt for agent deployment - agent, agentValues, err := promptAgentDeployment(chartValues, distributed, pbSecret.Namespace) + _, agentValues, err := promptAgentDeployment(chartValues, distributed, pbInfo.Name, pbInfo.Namespace) if err != nil { log.Fatalf("Failed to prompt for agent deployment: %v", err) } @@ -69,54 +88,102 @@ func Installer(_ Plan) (values *ValuesHolder, chartValues []string) { } // Prompt for object store configuration and get the final chart values - objectStoreConfig, storeConfigValues, err := promptStoreConfigs(store, storeValues) + objectStoreConfig, storeConfigs, err := promptStoreConfigs(store, storeValues) if err != nil { log.Fatalf("Failed to prompt for object store configuration: %v", err) } - if err := applyParseableSecret(pbSecret, store, objectStoreConfig); err != nil { + if err := applyParseableSecret(pbInfo, store, objectStoreConfig); err != nil { log.Fatalf("Failed to apply secret object store configuration: %v", err) } - valuesHolder := ValuesHolder{ - DeploymentType: distributed, - ObjectStoreConfig: objectStoreConfig, - LoggingAgent: loggingAgent(agent), - ParseableSecret: *pbSecret, + // Define the deployment configuration + config := HelmDeploymentConfig{ + ReleaseName: pbInfo.Name, + Namespace: pbInfo.Namespace, + RepoName: "parseable", + RepoURL: "https://charts.parseable.com", + ChartName: "parseable", + Version: "1.6.5", + Values: storeConfigs, + Verbose: verbose, + } + + if err := deployRelease(config); err != nil { + log.Fatalf("Failed to deploy parseable, err: %v", err) } - if err := writeParseableConfig(&valuesHolder); err != nil { - log.Fatalf("Failed to write Parseable configuration: %v", err) + if err := updateInstallerFile(installerEntry{ + Name: pbInfo.Name, + Namespace: pbInfo.Namespace, + Version: config.Version, + Status: "success", + }); err != nil { + log.Fatalf("Failed to update parseable installer file, err: %v", err) } + printSuccessBanner(pbInfo.Name, pbInfo.Namespace, string(distributed), config.Version, pbInfo.Username, pbInfo.Password) - return &valuesHolder, append(chartValues, storeConfigValues...) } -// promptStorageClass prompts the user to enter a Kubernetes storage class +// promptStorageClass fetches and prompts the user to select a Kubernetes storage class func promptStorageClass() (string, error) { - // Prompt user for storage class - fmt.Print(common.Yellow + "Enter the kubernetes storage class: " + common.Reset) - reader := bufio.NewReader(os.Stdin) - storageClass, err := reader.ReadString('\n') + // Load the kubeconfig from the default location + kubeconfig := clientcmd.NewDefaultClientConfigLoadingRules().GetDefaultFilename() + config, err := clientcmd.BuildConfigFromFlags("", kubeconfig) if err != nil { - return "", fmt.Errorf("failed to read storage class: %w", err) + return "", fmt.Errorf("failed to load kubeconfig: %w", err) } - storageClass = strings.TrimSpace(storageClass) + // Create a Kubernetes client + clientset, err := kubernetes.NewForConfig(config) + if err != nil { + return "", fmt.Errorf("failed to create Kubernetes client: %w", err) + } - // Validate that the storage class is not empty - if storageClass == "" { - return "", fmt.Errorf("storage class cannot be empty") + // Fetch the storage classes + storageClasses, err := clientset.StorageV1().StorageClasses().List(context.Background(), metav1.ListOptions{}) + if err != nil { + return "", fmt.Errorf("failed to fetch storage classes: %w", err) } - return storageClass, nil + // Extract the names of storage classes + var storageClassNames []string + for _, sc := range storageClasses.Items { + storageClassNames = append(storageClassNames, sc.Name) + } + + // Check if there are no storage classes available + if len(storageClassNames) == 0 { + return "", fmt.Errorf("no storage classes found in the cluster") + } + + // Use promptui to allow the user to select a storage class + prompt := promptui.Select{ + Label: "Select a Kubernetes storage class", + Items: storageClassNames, + } + + _, selectedStorageClass, err := prompt.Run() + if err != nil { + return "", fmt.Errorf("failed to select storage class: %w", err) + } + + return selectedStorageClass, nil } // promptNamespaceAndCredentials prompts the user for namespace and credentials -func promptNamespaceAndCredentials() (*ParseableSecret, error) { +func promptNamespaceAndCredentials() (*ParseableInfo, error) { + // Prompt user for release name + fmt.Print(common.Yellow + "Enter the Name for deployment: " + common.Reset) + reader := bufio.NewReader(os.Stdin) + name, err := reader.ReadString('\n') + if err != nil { + return nil, fmt.Errorf("failed to read namespace: %w", err) + } + name = strings.TrimSpace(name) + // Prompt user for namespace fmt.Print(common.Yellow + "Enter the Kubernetes namespace for deployment: " + common.Reset) - reader := bufio.NewReader(os.Stdin) namespace, err := reader.ReadString('\n') if err != nil { return nil, fmt.Errorf("failed to read namespace: %w", err) @@ -139,7 +206,8 @@ func promptNamespaceAndCredentials() (*ParseableSecret, error) { } password = strings.TrimSpace(password) - return &ParseableSecret{ + return &ParseableInfo{ + Name: name, Namespace: namespace, Username: username, Password: password, @@ -147,7 +215,7 @@ func promptNamespaceAndCredentials() (*ParseableSecret, error) { } // applyParseableSecret creates and applies the Kubernetes secret -func applyParseableSecret(ps *ParseableSecret, store ObjectStore, objectStoreConfig ObjectStoreConfig) error { +func applyParseableSecret(ps *ParseableInfo, store ObjectStore, objectStoreConfig ObjectStoreConfig) error { var secretManifest string if store == LocalStore { secretManifest = getParseableSecretLocal(ps) @@ -168,7 +236,7 @@ func applyParseableSecret(ps *ParseableSecret, store ObjectStore, objectStoreCon return nil } -func getParseableSecretBlob(ps *ParseableSecret, objectStore ObjectStoreConfig) string { +func getParseableSecretBlob(ps *ParseableInfo, objectStore ObjectStoreConfig) string { // Create the Secret manifest secretManifest := fmt.Sprintf(` apiVersion: v1 @@ -203,7 +271,7 @@ data: return secretManifest } -func getParseableSecretS3(ps *ParseableSecret, objectStore ObjectStoreConfig) string { +func getParseableSecretS3(ps *ParseableInfo, objectStore ObjectStoreConfig) string { // Create the Secret manifest secretManifest := fmt.Sprintf(` apiVersion: v1 @@ -239,7 +307,7 @@ data: return secretManifest } -func getParseableSecretGcs(ps *ParseableSecret, objectStore ObjectStoreConfig) string { +func getParseableSecretGcs(ps *ParseableInfo, objectStore ObjectStoreConfig) string { // Create the Secret manifest secretManifest := fmt.Sprintf(` apiVersion: v1 @@ -275,7 +343,7 @@ data: return secretManifest } -func getParseableSecretLocal(ps *ParseableSecret) string { +func getParseableSecretLocal(ps *ParseableInfo) string { // Create the Secret manifest secretManifest := fmt.Sprintf(` apiVersion: v1 @@ -303,7 +371,7 @@ data: } // promptAgentDeployment prompts the user for agent deployment options -func promptAgentDeployment(chartValues []string, deployment deploymentType, namespace string) (string, []string, error) { +func promptAgentDeployment(chartValues []string, deployment deploymentType, name, namespace string) (string, []string, error) { // Prompt for Agent Deployment type promptAgentSelect := promptui.Select{ Items: []string{string(fluentbit), string(vector), "I have my agent running / I'll set up later"}, @@ -322,11 +390,7 @@ func promptAgentDeployment(chartValues []string, deployment deploymentType, name if agentDeploymentType == string(vector) { chartValues = append(chartValues, "vector.enabled=true") } else if agentDeploymentType == string(fluentbit) { - if deployment == standalone { - chartValues = append(chartValues, "fluent-bit.serverHost=parseable."+namespace+".svc.cluster.local") - } else if deployment == distributed { - chartValues = append(chartValues, "fluent-bit.serverHost=parseable-ingestor-service."+namespace+".svc.cluster.local") - } + chartValues = append(chartValues, "fluent-bit.serverHost="+name+"-ingestor-service."+namespace+".svc.cluster.local") chartValues = append(chartValues, "fluent-bit.enabled=true") } @@ -527,30 +591,6 @@ func promptForInput(label string) string { return strings.TrimSpace(input) } -func writeParseableConfig(valuesHolder *ValuesHolder) error { - // Create config directory - configDir := filepath.Join(os.Getenv("HOME"), ".parseable") - if err := os.MkdirAll(configDir, 0o755); err != nil { - return fmt.Errorf("failed to create config directory: %w", err) - } - - // Define config file path - configPath := filepath.Join(configDir, valuesHolder.ParseableSecret.Namespace+".yaml") - - // Marshal values to YAML - configBytes, err := yamlv2.Marshal(valuesHolder) - if err != nil { - return fmt.Errorf("failed to marshal config to YAML: %w", err) - } - - // Write config file - if err := os.WriteFile(configPath, configBytes, 0o644); err != nil { - return fmt.Errorf("failed to write config file: %w", err) - } - - return nil -} - // promptK8sContext retrieves Kubernetes contexts from kubeconfig. func promptK8sContext() (clusterName string, err error) { kubeconfigPath := os.Getenv("KUBECONFIG") @@ -597,3 +637,245 @@ func promptK8sContext() (clusterName string, err error) { return clusterName, nil } + +// printBanner displays a welcome banner +func printBanner() { + banner := ` + -------------------------------------- + Welcome to Parseable OSS Installation + -------------------------------------- +` + fmt.Println(common.Green + banner + common.Reset) +} + +type HelmDeploymentConfig struct { + ReleaseName string + Namespace string + RepoName string + RepoURL string + ChartName string + Version string + Values []string + Verbose bool +} + +// deployRelease handles the deployment of a Helm release using a configuration struct +func deployRelease(config HelmDeploymentConfig) error { + // Helm application configuration + app := helm.Helm{ + ReleaseName: config.ReleaseName, + Namespace: config.Namespace, + RepoName: config.RepoName, + RepoURL: config.RepoURL, + ChartName: config.ChartName, + Version: config.Version, + Values: config.Values, + } + + // Create a spinner + msg := fmt.Sprintf(" Deploying parseable to release name [%s] namespace [%s]", config.ReleaseName, config.Namespace) + spinner := createDeploymentSpinner(config.Namespace, msg) + + // Redirect standard output if not in verbose mode + var oldStdout *os.File + if !config.Verbose { + oldStdout = os.Stdout + _, w, _ := os.Pipe() + os.Stdout = w + } + + spinner.Start() + + // Deploy using Helm + errCh := make(chan error, 1) + var wg sync.WaitGroup + wg.Add(1) + + go func() { + defer wg.Done() + if err := helm.Apply(app, config.Verbose); err != nil { + errCh <- err + } + }() + + wg.Wait() + close(errCh) + + // Stop the spinner and restore stdout + spinner.Stop() + if !config.Verbose { + os.Stdout = oldStdout + } + + // Check for errors + if err, ok := <-errCh; ok { + return err + } + + return nil +} + +// printSuccessBanner remains the same as in the original code +func printSuccessBanner(name, namespace, deployment, version, username, password string) { + var ingestionURL, serviceName string + if deployment == "standalone" { + ingestionURL = name + "." + namespace + ".svc.cluster.local" + serviceName = name + } else if deployment == "distributed" { + ingestionURL = name + "-ingestor-service." + namespace + ".svc.cluster.local" + serviceName = name + "-querier-service" + } + + // Encode credentials to Base64 + credentials := map[string]string{ + "username": username, + "password": password, + } + credentialsJSON, err := json.Marshal(credentials) + if err != nil { + fmt.Printf("failed to marshal credentials: %v\n", err) + return + } + + base64EncodedString := base64.StdEncoding.EncodeToString(credentialsJSON) + + fmt.Println("\n" + common.Green + "šŸŽ‰ Parseable Deployment Successful! šŸŽ‰" + common.Reset) + fmt.Println(strings.Repeat("=", 50)) + + fmt.Printf("%s Deployment Details:\n", common.Blue+"ā„¹ļø ") + fmt.Printf(" • Namespace: %s\n", common.Blue+namespace) + fmt.Printf(" • Chart Version: %s\n", common.Blue+version) + fmt.Printf(" • Ingestion URL: %s\n", ingestionURL) + + fmt.Println("\n" + common.Blue + "šŸ”— Resources:" + common.Reset) + fmt.Println(common.Blue + " • Documentation: https://www.parseable.com/docs/server/introduction") + fmt.Println(common.Blue + " • Stream Management: https://www.parseable.com/docs/server/api") + + fmt.Println("\n" + common.Blue + "Happy Logging!" + common.Reset) + + // Port-forward the service + localPort := "8001" + fmt.Printf(common.Green+"Port-forwarding %s service on port %s in namespace %s...\n"+common.Reset, serviceName, localPort, namespace) + + if err = startPortForward(namespace, serviceName, "80", localPort, false); err != nil { + fmt.Printf(common.Red+"failed to port-forward service: %s", err.Error()) + } + + // Redirect to UI + localURL := fmt.Sprintf("http://localhost:%s/login?q=%s", localPort, base64EncodedString) + fmt.Printf(common.Green+"Opening Parseable UI at %s\n"+common.Reset, localURL) + openBrowser(localURL) +} + +func startPortForward(namespace, serviceName, remotePort, localPort string, verbose bool) error { + // Build the port-forward command + cmd := exec.Command("kubectl", "port-forward", + fmt.Sprintf("svc/%s", serviceName), + fmt.Sprintf("%s:%s", localPort, remotePort), + "-n", namespace, + ) + + // Redirect the command's output to the standard output for debugging + if !verbose { + cmd.Stdout = nil // Suppress standard output + cmd.Stderr = nil // Suppress standard error + } else { + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + } + + // Run the command in the background + if err := cmd.Start(); err != nil { + return fmt.Errorf("failed to start port-forward: %w", err) + } + + // Run in a goroutine to keep it alive + go func() { + _ = cmd.Wait() + }() + + // Check connection on the forwarded port + retries := 10 + for i := 0; i < retries; i++ { + conn, err := net.Dial("tcp", fmt.Sprintf("localhost:%s", localPort)) + if err == nil { + conn.Close() // Connection successful, break out of the loop + fmt.Println(common.Green + "Port-forwarding successfully established!") + time.Sleep(5 * time.Second) // some delay + return nil + } + time.Sleep(3 * time.Second) // Wait before retrying + } + + // If we reach here, port-forwarding failed + cmd.Process.Kill() // Stop the kubectl process + return fmt.Errorf(common.Red+"failed to establish port-forward connection to localhost:%s", localPort) +} + +func openBrowser(url string) { + var cmd *exec.Cmd + switch os := runtime.GOOS; os { + case "windows": + cmd = exec.Command("rundll32", "url.dll,FileProtocolHandler", url) + case "darwin": + cmd = exec.Command("open", url) + case "linux": + cmd = exec.Command("xdg-open", url) + default: + fmt.Printf("Please open the following URL manually: %s\n", url) + return + } + cmd.Start() +} + +// 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 +} + +// updateInstallerFile updates or creates the installer.yaml file with deployment info +func updateInstallerFile(entry installerEntry) error { + // Define the file path + homeDir, err := os.UserHomeDir() + if err != nil { + return fmt.Errorf("failed to get user home directory: %w", err) + } + filePath := filepath.Join(homeDir, ".parseable", "installer.yaml") + + // Create the directory if it doesn't exist + if err := os.MkdirAll(filepath.Dir(filePath), 0755); err != nil { + return fmt.Errorf("failed to create directory for installer file: %w", err) + } + + // Read existing entries if the file exists + var entries []installerEntry + if _, err := os.Stat(filePath); err == nil { + // File exists, load existing content + data, err := os.ReadFile(filePath) + if err != nil { + return fmt.Errorf("failed to read existing installer file: %w", err) + } + + if err := yaml.Unmarshal(data, &entries); err != nil { + return fmt.Errorf("failed to parse existing installer file: %w", err) + } + } + + // Append the new entry + entries = append(entries, entry) + + // Write the updated entries back to the file + data, err := yamlv3.Marshal(entries) + if err != nil { + return fmt.Errorf("failed to marshal installer data: %w", err) + } + + if err := os.WriteFile(filePath, data, 0644); err != nil { + return fmt.Errorf("failed to write installer file: %w", err) + } + + return nil +} diff --git a/pkg/installer/model.go b/pkg/installer/model.go index 7710e9f..84d08c4 100644 --- a/pkg/installer/model.go +++ b/pkg/installer/model.go @@ -37,9 +37,10 @@ const ( _ loggingAgent = "I have my agent running / I'll set up later" ) -// ParseableSecret represents the secret used to authenticate with Parseable. -type ParseableSecret struct { - Namespace string // Namespace where the secret is located. +// ParseableInfo represents the info used to authenticate, metadata with Parseable. +type ParseableInfo struct { + Name string // Name for parseable + Namespace string // Namespace for parseable Username string // Username for authentication. Password string // Password for authentication. } @@ -92,11 +93,3 @@ type Blob struct { Container string // Container name in the Azure Blob store. URL string // URL of the Azure Blob store. } - -// ValuesHolder holds the configuration values required for deployment. -type ValuesHolder struct { - DeploymentType deploymentType // Deployment type (standalone or distributed). - ObjectStoreConfig ObjectStoreConfig // Configuration for the object storage backend. - LoggingAgent loggingAgent // Logging agent to be used. - ParseableSecret ParseableSecret // Secret used to authenticate with Parseable. -} diff --git a/pkg/installer/plans.go b/pkg/installer/plans.go index d37481e..8aa5d45 100644 --- a/pkg/installer/plans.go +++ b/pkg/installer/plans.go @@ -64,7 +64,7 @@ var Plans = map[string]Plan{ }, } -func PromptUserPlanSelection() (Plan, error) { +func promptUserPlanSelection() (Plan, error) { planList := []Plan{ Plans["Small"], Plans["Medium"], @@ -97,5 +97,13 @@ func PromptUserPlanSelection() (Plan, error) { return Plan{}, fmt.Errorf("failed to select deployment type: %w", err) } + fmt.Printf( + common.Cyan+" Ingestion Speed: %s\n"+ + common.Cyan+" Per Day Ingestion: %s\n"+ + common.Cyan+" Query Performance: %s\n"+ + common.Cyan+" CPU & Memory: %s\n"+ + common.Reset, planList[index].IngestionSpeed, planList[index].PerDayIngestion, + planList[index].QueryPerformance, planList[index].CPUAndMemorySpecs) + return planList[index], nil } diff --git a/pkg/installer/spinner.go b/pkg/installer/spinner.go index 6d51d01..a53fbf2 100644 --- a/pkg/installer/spinner.go +++ b/pkg/installer/spinner.go @@ -37,7 +37,7 @@ func createDeploymentSpinner(namespace, infoMsg string) *spinner.Spinner { spinner.WithSuffix(" ..."), ) - s.Prefix = fmt.Sprintf(common.Yellow+infoMsg+" %s ", namespace) + s.Prefix = fmt.Sprintf(common.Yellow + infoMsg) return s } diff --git a/pkg/installer/uninstaller.go b/pkg/installer/uninstaller.go index 5f0cb8c..6c559ef 100644 --- a/pkg/installer/uninstaller.go +++ b/pkg/installer/uninstaller.go @@ -15,159 +15,121 @@ package installer -import ( - "bufio" - "context" - "fmt" - "os" - "path/filepath" - "strings" - "time" - - "pb/pkg/common" - "pb/pkg/helm" - - "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 canceled.") - } - - // 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 -} +// func Uninstaller(verbose bool) error { +// // Load configuration from the parseable.yaml file +// configPath := filepath.Join(os.Getenv("HOME"), ".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 canceled.") +// } + +// // 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 +// } + +// // 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 +// } From 4e2336791afd67d127218436fbddd3abb91c2e59 Mon Sep 17 00:00:00 2001 From: AdheipSingh Date: Thu, 19 Dec 2024 10:36:12 +0530 Subject: [PATCH 02/13] add list ooss --- cmd/list.go | 86 ++++++++++++++++++++++++++++++++++++++ go.mod | 1 + go.sum | 3 ++ main.go | 19 +++++++++ pkg/installer/installer.go | 10 ++--- 5 files changed, 114 insertions(+), 5 deletions(-) create mode 100644 cmd/list.go diff --git a/cmd/list.go b/cmd/list.go new file mode 100644 index 0000000..10075fb --- /dev/null +++ b/cmd/list.go @@ -0,0 +1,86 @@ +package cmd + +// 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 . + +import ( + "fmt" + "log" + "os" + "path/filepath" + "pb/pkg/installer" + + "github.com/olekukonko/tablewriter" + "github.com/spf13/cobra" + "gopkg.in/yaml.v2" +) + +// ListOssCmd lists the Parseable OSS servers +var ListOssCmd = &cobra.Command{ + Use: "oss", + Short: "List available Parseable OSS servers", + Example: "pb list oss", + Run: func(cmd *cobra.Command, _ []string) { + // Read the installer file + entries, err := readInstallerFile() + if err != nil { + log.Fatalf("Failed to list OSS servers: %v", err) + } + + // Check if there are no entries + if len(entries) == 0 { + fmt.Println("No OSS servers found.") + return + } + + // Display the entries in a table format + table := tablewriter.NewWriter(os.Stdout) + table.SetHeader([]string{"Name", "Namespace", "Version", "Status"}) + + for _, entry := range entries { + table.Append([]string{entry.Name, entry.Namespace, entry.Version, entry.Status}) + } + + table.Render() + }, +} + +// readInstallerFile reads and parses the installer.yaml file +func readInstallerFile() ([]installer.InstallerEntry, error) { + // Define the file path + homeDir, err := os.UserHomeDir() + if err != nil { + return nil, fmt.Errorf("failed to get user home directory: %w", err) + } + filePath := filepath.Join(homeDir, ".parseable", "installer.yaml") + + // Check if the file exists + if _, err := os.Stat(filePath); os.IsNotExist(err) { + return nil, fmt.Errorf("installer file not found at %s", filePath) + } + + // Read and parse the file + data, err := os.ReadFile(filePath) + if err != nil { + return nil, fmt.Errorf("failed to read installer file: %w", err) + } + + var entries []installer.InstallerEntry + if err := yaml.Unmarshal(data, &entries); err != nil { + return nil, fmt.Errorf("failed to parse installer file: %w", err) + } + + return entries, nil +} diff --git a/go.mod b/go.mod index 53e13b4..dd0e3c8 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/gofrs/flock v0.12.1 github.com/manifoldco/promptui v0.9.0 github.com/oklog/ulid/v2 v2.1.0 + github.com/olekukonko/tablewriter v0.0.5 github.com/pkg/errors v0.9.1 github.com/spf13/pflag v1.0.5 golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8 diff --git a/go.sum b/go.sum index 41a3813..1d77164 100644 --- a/go.sum +++ b/go.sum @@ -294,6 +294,7 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= @@ -342,6 +343,8 @@ github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU= github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= diff --git a/main.go b/main.go index 02e8f97..11f06d4 100644 --- a/main.go +++ b/main.go @@ -203,6 +203,23 @@ var install = &cobra.Command{ }, } +var list = &cobra.Command{ + Use: "list", + Short: "List parseable on kubernetes cluster", + Long: "\nlist command is used to list parseable oss installations.", + 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, "install", args) + }() + }, +} + var uninstall = &cobra.Command{ Use: "uninstall", Short: "Uninstall parseable on kubernetes cluster", @@ -247,6 +264,7 @@ func main() { schema.AddCommand(pb.CreateSchemaCmd) install.AddCommand(pb.InstallOssCmd) + list.AddCommand(pb.ListOssCmd) uninstall.AddCommand(pb.UnInstallOssCmd) @@ -261,6 +279,7 @@ func main() { cli.AddCommand(install) cli.AddCommand(uninstall) cli.AddCommand(schema) + cli.AddCommand(list) // Set as command pb.VersionCmd.Run = func(_ *cobra.Command, _ []string) { diff --git a/pkg/installer/installer.go b/pkg/installer/installer.go index ca4ea52..ee808df 100644 --- a/pkg/installer/installer.go +++ b/pkg/installer/installer.go @@ -113,7 +113,7 @@ func waterFall(verbose bool) { log.Fatalf("Failed to deploy parseable, err: %v", err) } - if err := updateInstallerFile(installerEntry{ + if err := updateInstallerFile(InstallerEntry{ Name: pbInfo.Name, Namespace: pbInfo.Namespace, Version: config.Version, @@ -828,8 +828,8 @@ func openBrowser(url string) { cmd.Start() } -// installerEntry represents an entry in the installer.yaml file -type installerEntry struct { +// InstallerEntry represents an entry in the installer.yaml file +type InstallerEntry struct { Name string `yaml:"name"` Namespace string `yaml:"namespace"` Version string `yaml:"version"` @@ -837,7 +837,7 @@ type installerEntry struct { } // updateInstallerFile updates or creates the installer.yaml file with deployment info -func updateInstallerFile(entry installerEntry) error { +func updateInstallerFile(entry InstallerEntry) error { // Define the file path homeDir, err := os.UserHomeDir() if err != nil { @@ -851,7 +851,7 @@ func updateInstallerFile(entry installerEntry) error { } // Read existing entries if the file exists - var entries []installerEntry + var entries []InstallerEntry if _, err := os.Stat(filePath); err == nil { // File exists, load existing content data, err := os.ReadFile(filePath) From e9ace0fa76ee292f7a7991aeef96fa47d6d39b5c Mon Sep 17 00:00:00 2001 From: AdheipSingh Date: Fri, 20 Dec 2024 07:27:11 +0530 Subject: [PATCH 03/13] installer configured with plans, svc bug fixed --- pkg/installer/installer.go | 103 +++++++++++++++++++++++++++---------- pkg/installer/plans.go | 16 +++--- 2 files changed, 84 insertions(+), 35 deletions(-) diff --git a/pkg/installer/installer.go b/pkg/installer/installer.go index ee808df..7172dc8 100644 --- a/pkg/installer/installer.go +++ b/pkg/installer/installer.go @@ -57,7 +57,7 @@ func Installer(verbose bool) { // waterFall orchestrates the installation process func waterFall(verbose bool) { var chartValues []string - _, err := promptUserPlanSelection() + plan, err := promptUserPlanSelection() if err != nil { log.Fatalf("Failed to prompt for plan selection: %v", err) } @@ -76,7 +76,7 @@ func waterFall(verbose bool) { } // Prompt for agent deployment - _, agentValues, err := promptAgentDeployment(chartValues, distributed, pbInfo.Name, pbInfo.Namespace) + _, agentValues, err := promptAgentDeployment(chartValues, distributed, *pbInfo) if err != nil { log.Fatalf("Failed to prompt for agent deployment: %v", err) } @@ -88,7 +88,7 @@ func waterFall(verbose bool) { } // Prompt for object store configuration and get the final chart values - objectStoreConfig, storeConfigs, err := promptStoreConfigs(store, storeValues) + objectStoreConfig, storeConfigs, err := promptStoreConfigs(store, storeValues, plan) if err != nil { log.Fatalf("Failed to prompt for object store configuration: %v", err) } @@ -104,7 +104,7 @@ func waterFall(verbose bool) { RepoName: "parseable", RepoURL: "https://charts.parseable.com", ChartName: "parseable", - Version: "1.6.5", + Version: "1.6.6", Values: storeConfigs, Verbose: verbose, } @@ -121,7 +121,7 @@ func waterFall(verbose bool) { }); err != nil { log.Fatalf("Failed to update parseable installer file, err: %v", err) } - printSuccessBanner(pbInfo.Name, pbInfo.Namespace, string(distributed), config.Version, pbInfo.Username, pbInfo.Password) + printSuccessBanner(*pbInfo, config.Version) } @@ -371,7 +371,7 @@ data: } // promptAgentDeployment prompts the user for agent deployment options -func promptAgentDeployment(chartValues []string, deployment deploymentType, name, namespace string) (string, []string, error) { +func promptAgentDeployment(chartValues []string, deployment deploymentType, pbInfo ParseableInfo) (string, []string, error) { // Prompt for Agent Deployment type promptAgentSelect := promptui.Select{ Items: []string{string(fluentbit), string(vector), "I have my agent running / I'll set up later"}, @@ -387,11 +387,32 @@ func promptAgentDeployment(chartValues []string, deployment deploymentType, name return "", nil, fmt.Errorf("failed to prompt for agent deployment type: %w", err) } - if agentDeploymentType == string(vector) { - chartValues = append(chartValues, "vector.enabled=true") - } else if agentDeploymentType == string(fluentbit) { - chartValues = append(chartValues, "fluent-bit.serverHost="+name+"-ingestor-service."+namespace+".svc.cluster.local") + ingestorUrl, _ := getParseableSvcUrls(pbInfo.Name, pbInfo.Namespace) + + if agentDeploymentType == string(fluentbit) { + chartValues = append(chartValues, "fluent-bit.serverHost="+ingestorUrl) + chartValues = append(chartValues, "fluent-bit.serverUsername="+pbInfo.Username) + chartValues = append(chartValues, "fluent-bit.serverPassword="+pbInfo.Password) + chartValues = append(chartValues, "fluent-bit.serverStream="+"$NAMESPACE") + + // Prompt for namespaces to exclude + promptExcludeNamespaces := promptui.Prompt{ + Label: "Enter namespaces to exclude from collection (comma-separated, e.g., kube-system,default): ", + Templates: &promptui.PromptTemplates{ + Prompt: "{{ `Namespaces to exclude` | yellow }}: ", + Valid: "{{ `` | green }}: {{ . | yellow }}", + Invalid: "{{ `Invalid input` | red }}", + }, + } + excludeNamespaces, err := promptExcludeNamespaces.Run() + if err != nil { + return "", nil, fmt.Errorf("failed to prompt for exclude namespaces: %w", err) + } + + chartValues = append(chartValues, "fluent-bit.excludeNamespaces="+strings.ReplaceAll(excludeNamespaces, ",", "\\,")) chartValues = append(chartValues, "fluent-bit.enabled=true") + } else if agentDeploymentType == string(vector) { + chartValues = append(chartValues, "vector.enabled=true") } return agentDeploymentType, chartValues, nil @@ -423,7 +444,14 @@ func promptStore(chartValues []string) (ObjectStore, []string, error) { } // promptStoreConfigs prompts for object store configurations and appends chart values -func promptStoreConfigs(store ObjectStore, chartValues []string) (ObjectStoreConfig, []string, error) { +func promptStoreConfigs(store ObjectStore, chartValues []string, plan Plan) (ObjectStoreConfig, []string, error) { + + cpuIngestors := "parseable.highAvailability.ingestor.resources.limits.cpu=" + plan.CPU + memoryIngestors := "parseable.highAvailability.ingestor.resources.limits.memory=" + plan.Memory + + cpuQuery := "parseable.resources.limits.cpu=" + plan.CPU + memoryQuery := "parseable.resources.limits.memory=" + plan.Memory + // Initialize a struct to hold store values var storeValues ObjectStoreConfig @@ -448,7 +476,13 @@ func promptStoreConfigs(store ObjectStore, chartValues []string) (ObjectStoreCon chartValues = append(chartValues, "parseable.store="+string(S3Store)) chartValues = append(chartValues, "parseable.s3ModeSecret.enabled=true") chartValues = append(chartValues, "parseable.persistence.staging.enabled=true") + chartValues = append(chartValues, "parseable.persistence.staging.size=5Gi") chartValues = append(chartValues, "parseable.persistence.staging.storageClass="+sc) + chartValues = append(chartValues, cpuIngestors) + chartValues = append(chartValues, memoryIngestors) + chartValues = append(chartValues, cpuQuery) + chartValues = append(chartValues, memoryQuery) + return storeValues, chartValues, nil case BlobStore: sc, err := promptStorageClass() @@ -464,7 +498,12 @@ func promptStoreConfigs(store ObjectStore, chartValues []string) (ObjectStoreCon chartValues = append(chartValues, "parseable.store="+string(BlobStore)) chartValues = append(chartValues, "parseable.blobModeSecret.enabled=true") chartValues = append(chartValues, "parseable.persistence.staging.enabled=true") + chartValues = append(chartValues, "parseable.persistence.staging.size=5Gi") chartValues = append(chartValues, "parseable.persistence.staging.storageClass="+sc) + chartValues = append(chartValues, cpuIngestors) + chartValues = append(chartValues, memoryIngestors) + chartValues = append(chartValues, cpuQuery) + chartValues = append(chartValues, memoryQuery) return storeValues, chartValues, nil case GcsStore: sc, err := promptStorageClass() @@ -483,7 +522,12 @@ func promptStoreConfigs(store ObjectStore, chartValues []string) (ObjectStoreCon chartValues = append(chartValues, "parseable.store="+string(GcsStore)) chartValues = append(chartValues, "parseable.gcsModeSecret.enabled=true") chartValues = append(chartValues, "parseable.persistence.staging.enabled=true") + chartValues = append(chartValues, "parseable.persistence.staging.size=5Gi") chartValues = append(chartValues, "parseable.persistence.staging.storageClass="+sc) + chartValues = append(chartValues, cpuIngestors) + chartValues = append(chartValues, memoryIngestors) + chartValues = append(chartValues, cpuQuery) + chartValues = append(chartValues, memoryQuery) return storeValues, chartValues, nil } @@ -673,7 +717,7 @@ func deployRelease(config HelmDeploymentConfig) error { } // Create a spinner - msg := fmt.Sprintf(" Deploying parseable to release name [%s] namespace [%s]", config.ReleaseName, config.Namespace) + msg := fmt.Sprintf(" Deploying parseable release name [%s] namespace [%s] ", config.ReleaseName, config.Namespace) spinner := createDeploymentSpinner(config.Namespace, msg) // Redirect standard output if not in verbose mode @@ -716,20 +760,14 @@ func deployRelease(config HelmDeploymentConfig) error { } // printSuccessBanner remains the same as in the original code -func printSuccessBanner(name, namespace, deployment, version, username, password string) { - var ingestionURL, serviceName string - if deployment == "standalone" { - ingestionURL = name + "." + namespace + ".svc.cluster.local" - serviceName = name - } else if deployment == "distributed" { - ingestionURL = name + "-ingestor-service." + namespace + ".svc.cluster.local" - serviceName = name + "-querier-service" - } +func printSuccessBanner(pbInfo ParseableInfo, version string) { + + ingestionUrl, queryUrl := getParseableSvcUrls(pbInfo.Name, pbInfo.Namespace) // Encode credentials to Base64 credentials := map[string]string{ - "username": username, - "password": password, + "username": pbInfo.Username, + "password": pbInfo.Password, } credentialsJSON, err := json.Marshal(credentials) if err != nil { @@ -743,9 +781,9 @@ func printSuccessBanner(name, namespace, deployment, version, username, password fmt.Println(strings.Repeat("=", 50)) fmt.Printf("%s Deployment Details:\n", common.Blue+"ā„¹ļø ") - fmt.Printf(" • Namespace: %s\n", common.Blue+namespace) + fmt.Printf(" • Namespace: %s\n", common.Blue+pbInfo.Namespace) fmt.Printf(" • Chart Version: %s\n", common.Blue+version) - fmt.Printf(" • Ingestion URL: %s\n", ingestionURL) + fmt.Printf(" • Ingestion URL: %s\n", ingestionUrl) fmt.Println("\n" + common.Blue + "šŸ”— Resources:" + common.Reset) fmt.Println(common.Blue + " • Documentation: https://www.parseable.com/docs/server/introduction") @@ -755,9 +793,9 @@ func printSuccessBanner(name, namespace, deployment, version, username, password // Port-forward the service localPort := "8001" - fmt.Printf(common.Green+"Port-forwarding %s service on port %s in namespace %s...\n"+common.Reset, serviceName, localPort, namespace) + fmt.Printf(common.Green+"Port-forwarding %s service on port %s in namespace %s...\n"+common.Reset, queryUrl, localPort, pbInfo.Namespace) - if err = startPortForward(namespace, serviceName, "80", localPort, false); err != nil { + if err = startPortForward(pbInfo.Namespace, queryUrl, "80", localPort, false); err != nil { fmt.Printf(common.Red+"failed to port-forward service: %s", err.Error()) } @@ -879,3 +917,14 @@ func updateInstallerFile(entry InstallerEntry) error { return nil } + +func getParseableSvcUrls(releaseName, namespace string) (ingestorUrl, queryUrl string) { + if releaseName == "parseable" { + ingestorUrl = releaseName + "-ingestor-service." + namespace + ".svc.cluster.local" + queryUrl = releaseName + "-querier-service" + return ingestorUrl, queryUrl + } + ingestorUrl = releaseName + "-parseable-ingestor-service." + namespace + ".svc.cluster.local" + queryUrl = releaseName + "-parseable-querier-service" + return ingestorUrl, queryUrl +} diff --git a/pkg/installer/plans.go b/pkg/installer/plans.go index 8aa5d45..f951b62 100644 --- a/pkg/installer/plans.go +++ b/pkg/installer/plans.go @@ -29,8 +29,8 @@ type Plan struct { PerDayIngestion string QueryPerformance string CPUAndMemorySpecs string - CPU int - Memory int + CPU string + Memory string } // Plans define the plans with clear CPU and memory specs for consumption @@ -41,8 +41,8 @@ var Plans = map[string]Plan{ PerDayIngestion: "~10GB", QueryPerformance: "Basic performance", CPUAndMemorySpecs: "2 CPUs, 4GB RAM", - CPU: 2, - Memory: 4, + CPU: "2", + Memory: "4Gi", }, "Medium": { Name: "Medium", @@ -50,8 +50,8 @@ var Plans = map[string]Plan{ PerDayIngestion: "~100GB", QueryPerformance: "Moderate performance", CPUAndMemorySpecs: "4 CPUs, 16GB RAM", - CPU: 4, - Memory: 16, + CPU: "4", + Memory: "16Gi", }, "Large": { Name: "Large", @@ -59,8 +59,8 @@ var Plans = map[string]Plan{ PerDayIngestion: "~1TB", QueryPerformance: "High performance", CPUAndMemorySpecs: "8 CPUs, 32GB RAM", - CPU: 8, - Memory: 32, + CPU: "8", + Memory: "32Gi", }, } From 2a86d6c5fa999ef4485aebcacdd3a48aa97ad4d3 Mon Sep 17 00:00:00 2001 From: AdheipSingh Date: Fri, 20 Dec 2024 08:11:45 +0530 Subject: [PATCH 04/13] add uninstall oss --- cmd/list.go | 13 +- cmd/uninstaller.go | 13 +- pkg/installer/installer.go | 14 +- pkg/installer/model.go | 9 ++ pkg/installer/uninstaller.go | 294 +++++++++++++++++++++-------------- 5 files changed, 205 insertions(+), 138 deletions(-) diff --git a/cmd/list.go b/cmd/list.go index 10075fb..60fd165 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -20,6 +20,7 @@ import ( "log" "os" "path/filepath" + "pb/pkg/common" "pb/pkg/installer" "github.com/olekukonko/tablewriter" @@ -47,10 +48,10 @@ var ListOssCmd = &cobra.Command{ // Display the entries in a table format table := tablewriter.NewWriter(os.Stdout) - table.SetHeader([]string{"Name", "Namespace", "Version", "Status"}) + table.SetHeader([]string{"Name", "Namespace", "Version", "Kubernetes Context", "Status"}) for _, entry := range entries { - table.Append([]string{entry.Name, entry.Namespace, entry.Version, entry.Status}) + table.Append([]string{entry.Name, entry.Namespace, entry.Version, entry.Context, entry.Status}) } table.Render() @@ -64,11 +65,15 @@ func readInstallerFile() ([]installer.InstallerEntry, error) { if err != nil { return nil, fmt.Errorf("failed to get user home directory: %w", err) } - filePath := filepath.Join(homeDir, ".parseable", "installer.yaml") + filePath := filepath.Join(homeDir, ".parseable", "pb", "installer.yaml") // Check if the file exists if _, err := os.Stat(filePath); os.IsNotExist(err) { - return nil, fmt.Errorf("installer file not found at %s", filePath) + 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 } // Read and parse the file diff --git a/cmd/uninstaller.go b/cmd/uninstaller.go index 6d8ca39..64cdec5 100644 --- a/cmd/uninstaller.go +++ b/cmd/uninstaller.go @@ -16,6 +16,10 @@ package cmd import ( + "fmt" + "pb/pkg/common" + "pb/pkg/installer" + "github.com/spf13/cobra" ) @@ -27,12 +31,9 @@ var UnInstallOssCmd = &cobra.Command{ // 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()) - // } + if err := installer.Uninstaller(verbose); err != nil { + fmt.Println(common.Red + err.Error()) + } return nil }, diff --git a/pkg/installer/installer.go b/pkg/installer/installer.go index 7172dc8..c2fc043 100644 --- a/pkg/installer/installer.go +++ b/pkg/installer/installer.go @@ -62,7 +62,8 @@ func waterFall(verbose bool) { log.Fatalf("Failed to prompt for plan selection: %v", err) } - if _, err := promptK8sContext(); err != nil { + context, err := promptK8sContext() + if err != nil { log.Fatalf("Failed to prompt for kubernetes context: %v", err) } @@ -117,6 +118,7 @@ func waterFall(verbose bool) { Name: pbInfo.Name, Namespace: pbInfo.Namespace, Version: config.Version, + Context: context, Status: "success", }); err != nil { log.Fatalf("Failed to update parseable installer file, err: %v", err) @@ -866,14 +868,6 @@ func openBrowser(url string) { cmd.Start() } -// 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 -} - // updateInstallerFile updates or creates the installer.yaml file with deployment info func updateInstallerFile(entry InstallerEntry) error { // Define the file path @@ -881,7 +875,7 @@ func updateInstallerFile(entry InstallerEntry) error { if err != nil { return fmt.Errorf("failed to get user home directory: %w", err) } - filePath := filepath.Join(homeDir, ".parseable", "installer.yaml") + filePath := filepath.Join(homeDir, ".parseable", "pb", "installer.yaml") // Create the directory if it doesn't exist if err := os.MkdirAll(filepath.Dir(filePath), 0755); err != nil { diff --git a/pkg/installer/model.go b/pkg/installer/model.go index 84d08c4..b9f3572 100644 --- a/pkg/installer/model.go +++ b/pkg/installer/model.go @@ -93,3 +93,12 @@ 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"` + Context string `yaml:"context"` + Status string `yaml:"status"` // todo ideally should be a heartbeat +} diff --git a/pkg/installer/uninstaller.go b/pkg/installer/uninstaller.go index 6c559ef..47a3816 100644 --- a/pkg/installer/uninstaller.go +++ b/pkg/installer/uninstaller.go @@ -15,121 +15,179 @@ package installer -// func Uninstaller(verbose bool) error { -// // Load configuration from the parseable.yaml file -// configPath := filepath.Join(os.Getenv("HOME"), ".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 canceled.") -// } - -// // 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 -// } - -// // 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 -// } +import ( + "bufio" + "context" + "fmt" + "os" + "path/filepath" + "pb/pkg/common" + "pb/pkg/helm" + "strings" + "time" + + "github.com/manifoldco/promptui" + apierrors "k8s.io/apimachinery/pkg/api/errors" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/yaml" + "k8s.io/client-go/kubernetes" +) + +// Uninstaller uninstalls Parseable from the selected cluster +func Uninstaller(verbose bool) error { + // Define the installer file path + homeDir, err := os.UserHomeDir() + if err != nil { + return fmt.Errorf("failed to get user home directory: %w", err) + } + installerFilePath := filepath.Join(homeDir, ".parseable", "pb", "installer.yaml") + + // Read the installer file + data, err := os.ReadFile(installerFilePath) + if err != nil { + return fmt.Errorf("failed to read installer file: %w", err) + } + + // Unmarshal the installer file content + var entries []InstallerEntry + if err := yaml.Unmarshal(data, &entries); err != nil { + return fmt.Errorf("failed to parse installer file: %w", err) + } + + // Prompt the user to select a cluster + clusterNames := make([]string, len(entries)) + for i, entry := range entries { + clusterNames[i] = fmt.Sprintf("[Name: %s] [Namespace: %s] [Context: %s]", entry.Name, entry.Namespace, entry.Context) + } + + promptClusterSelect := promptui.Select{ + Label: "Select a cluster to delete", + Items: clusterNames, + Templates: &promptui.SelectTemplates{ + Label: "{{ `Select Cluster` | yellow }}", + Active: "ā–ø {{ . | yellow }}", // Yellow arrow for active selection + Inactive: " {{ . | yellow }}", + Selected: "{{ `Selected:` | green }} {{ . | green }}", + }, + } + + index, _, err := promptClusterSelect.Run() + if err != nil { + return fmt.Errorf("failed to prompt for cluster selection: %v", err) + } + + selectedCluster := entries[index] + + // Display a warning banner + fmt.Println("\n────────────────────────────────────────────────────────────────────────────") + fmt.Println("āš ļø Deleting this cluster will not delete any data on object storage.") + fmt.Println(" This operation will clean up the Parseable deployment on Kubernetes.") + fmt.Println("────────────────────────────────────────────────────────────────────────────") + + // Confirm deletion + confirm, err := promptUserConfirmation(fmt.Sprintf(common.Yellow+"Do you still want to proceed with deleting the cluster '%s'?", selectedCluster.Name)) + if err != nil { + return fmt.Errorf("failed to get user confirmation: %v", err) + } + if !confirm { + fmt.Println(common.Yellow + "Uninstall canceled." + common.Reset) + return nil + } + + // Helm application configuration + helmApp := helm.Helm{ + ReleaseName: selectedCluster.Name, + Namespace: selectedCluster.Namespace, + RepoName: "parseable", + RepoURL: "https://charts.parseable.com", + ChartName: "parseable", + Version: selectedCluster.Version, + } + + // Create a spinner + spinner := createDeploymentSpinner(selectedCluster.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) + } + + // Call to clean up the secret instead of the namespace + fmt.Printf(common.Yellow+"Cleaning up 'parseable-env-secret' in namespace '%s'...\n"+common.Reset, selectedCluster.Namespace) + cleanupErr := cleanupParseableSecret(selectedCluster.Namespace) + if cleanupErr != nil { + return fmt.Errorf("failed to clean up secret in namespace '%s': %v", selectedCluster.Namespace, cleanupErr) + } + + // Print success banner + fmt.Printf(common.Green+"Successfully uninstalled Parseable from namespace '%s'.\n"+common.Reset, selectedCluster.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 +} + +// cleanupParseableSecret deletes the "parseable-env-secret" in the specified namespace using Kubernetes client-go +func cleanupParseableSecret(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 secret deletion + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + // Define the secret name + secretName := "parseable-env-secret" + + // Delete the secret + err = clientset.CoreV1().Secrets(namespace).Delete(ctx, secretName, v1.DeleteOptions{}) + if err != nil { + if apierrors.IsNotFound(err) { + fmt.Printf("Secret '%s' not found in namespace '%s'. Nothing to delete.\n", secretName, namespace) + return nil + } + return fmt.Errorf("error deleting secret '%s' in namespace '%s': %v", secretName, namespace, err) + } + + // Confirm the deletion + fmt.Printf("Secret '%s' successfully deleted from namespace '%s'.\n", secretName, namespace) + + return nil +} From c835c1d9fd1e8cba0531951691d869f45e456297 Mon Sep 17 00:00:00 2001 From: AdheipSingh Date: Sat, 21 Dec 2024 11:39:20 +0530 Subject: [PATCH 05/13] add list --- cmd/list.go | 84 ++++++++++++---------- pkg/installer/installer.go | 132 +++++++++++++++++++++++++---------- pkg/installer/model.go | 1 - pkg/installer/uninstaller.go | 2 +- 4 files changed, 146 insertions(+), 73 deletions(-) diff --git a/cmd/list.go b/cmd/list.go index 60fd165..7fac778 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -1,31 +1,21 @@ package cmd -// 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 . - import ( + "context" "fmt" "log" "os" - "path/filepath" + "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 @@ -34,8 +24,13 @@ var ListOssCmd = &cobra.Command{ Short: "List available Parseable OSS servers", Example: "pb list oss", Run: func(cmd *cobra.Command, _ []string) { - // Read the installer file - entries, err := readInstallerFile() + _, err := installer.PromptK8sContext() + if err != nil { + log.Fatalf("Failed to prompt for kubernetes context: %v", err) + } + + // Read the installer data from the ConfigMap + entries, err := readInstallerConfigMap() if err != nil { log.Fatalf("Failed to list OSS servers: %v", err) } @@ -48,27 +43,44 @@ var ListOssCmd = &cobra.Command{ // Display the entries in a table format table := tablewriter.NewWriter(os.Stdout) - table.SetHeader([]string{"Name", "Namespace", "Version", "Kubernetes Context", "Status"}) + table.SetHeader([]string{"Name", "Namespace", "Version", "Status"}) for _, entry := range entries { - table.Append([]string{entry.Name, entry.Namespace, entry.Version, entry.Context, entry.Status}) + table.Append([]string{entry.Name, entry.Namespace, entry.Version, entry.Status}) } table.Render() }, } -// readInstallerFile reads and parses the installer.yaml file -func readInstallerFile() ([]installer.InstallerEntry, error) { - // Define the file path - homeDir, err := os.UserHomeDir() +// 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 get user home directory: %w", err) + return nil, fmt.Errorf("failed to create Kubernetes client: %w", err) } - filePath := filepath.Join(homeDir, ".parseable", "pb", "installer.yaml") - // Check if the file exists - if _, err := os.Stat(filePath); os.IsNotExist(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`") @@ -76,16 +88,16 @@ func readInstallerFile() ([]installer.InstallerEntry, error) { return nil, nil } - // Read and parse the file - data, err := os.ReadFile(filePath) - if err != nil { - return nil, fmt.Errorf("failed to read installer file: %w", err) - } - var entries []installer.InstallerEntry - if err := yaml.Unmarshal(data, &entries); err != nil { - return nil, fmt.Errorf("failed to parse installer file: %w", err) + 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/pkg/installer/installer.go b/pkg/installer/installer.go index c2fc043..a45c205 100644 --- a/pkg/installer/installer.go +++ b/pkg/installer/installer.go @@ -26,7 +26,6 @@ import ( "net" "os" "os/exec" - "path/filepath" "runtime" "strings" "sync" @@ -36,7 +35,9 @@ import ( "pb/pkg/helm" "github.com/manifoldco/promptui" - yamlv3 "gopkg.in/yaml.v3" + yamling "gopkg.in/yaml.v3" + v1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" @@ -62,7 +63,7 @@ func waterFall(verbose bool) { log.Fatalf("Failed to prompt for plan selection: %v", err) } - context, err := promptK8sContext() + _, err = PromptK8sContext() if err != nil { log.Fatalf("Failed to prompt for kubernetes context: %v", err) } @@ -110,19 +111,17 @@ func waterFall(verbose bool) { Verbose: verbose, } - if err := deployRelease(config); err != nil { - log.Fatalf("Failed to deploy parseable, err: %v", err) - } - - if err := updateInstallerFile(InstallerEntry{ + if err := updateInstallerConfigMap(InstallerEntry{ Name: pbInfo.Name, Namespace: pbInfo.Namespace, Version: config.Version, - Context: context, Status: "success", }); err != nil { log.Fatalf("Failed to update parseable installer file, err: %v", err) } + if err := deployRelease(config); err != nil { + log.Fatalf("Failed to deploy parseable, err: %v", err) + } printSuccessBanner(*pbInfo, config.Version) } @@ -637,8 +636,8 @@ func promptForInput(label string) string { return strings.TrimSpace(input) } -// promptK8sContext retrieves Kubernetes contexts from kubeconfig. -func promptK8sContext() (clusterName string, err error) { +// PromptK8sContext retrieves Kubernetes contexts from kubeconfig. +func PromptK8sContext() (clusterName string, err error) { kubeconfigPath := os.Getenv("KUBECONFIG") if kubeconfigPath == "" { kubeconfigPath = os.Getenv("HOME") + "/.kube/config" @@ -868,45 +867,108 @@ func openBrowser(url string) { cmd.Start() } -// updateInstallerFile updates or creates the installer.yaml file with deployment info -func updateInstallerFile(entry InstallerEntry) error { - // Define the file path - homeDir, err := os.UserHomeDir() +func updateInstallerConfigMap(entry 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 fmt.Errorf("failed to get user home directory: %w", err) + return fmt.Errorf("failed to load kubeconfig: %w", err) } - filePath := filepath.Join(homeDir, ".parseable", "pb", "installer.yaml") - // Create the directory if it doesn't exist - if err := os.MkdirAll(filepath.Dir(filePath), 0755); err != nil { - return fmt.Errorf("failed to create directory for installer file: %w", err) + clientset, err := kubernetes.NewForConfig(config) + if err != nil { + return fmt.Errorf("failed to create Kubernetes client: %w", err) } - // Read existing entries if the file exists - var entries []InstallerEntry - if _, err := os.Stat(filePath); err == nil { - // File exists, load existing content - data, err := os.ReadFile(filePath) - if err != nil { - return fmt.Errorf("failed to read existing installer file: %w", err) + // Ensure the namespace exists + _, err = clientset.CoreV1().Namespaces().Get(context.TODO(), namespace, metav1.GetOptions{}) + if err != nil { + if apierrors.IsNotFound(err) { + _, err = clientset.CoreV1().Namespaces().Create(context.TODO(), &v1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: namespace, + }, + }, metav1.CreateOptions{}) + if err != nil { + return fmt.Errorf("failed to create namespace: %v", err) + } + } else { + return fmt.Errorf("failed to check namespace existence: %v", err) } + } - if err := yaml.Unmarshal(data, &entries); err != nil { - return fmt.Errorf("failed to parse existing installer file: %w", err) + // Create a dynamic Kubernetes client + dynamicClient, err := dynamic.NewForConfig(config) + if err != nil { + return fmt.Errorf("failed to create dynamic client: %w", err) + } + + // Define the ConfigMap resource + configMapResource := schema.GroupVersionResource{ + Group: "", // Core resources have an empty group + Version: "v1", + Resource: "configmaps", + } + + // Fetch the existing ConfigMap or initialize a new one + cm, err := dynamicClient.Resource(configMapResource).Namespace(namespace).Get(context.TODO(), configMapName, metav1.GetOptions{}) + var data map[string]interface{} + if err != nil { + if !apierrors.IsNotFound(err) { + return fmt.Errorf("failed to fetch ConfigMap: %v", err) + } + // If not found, initialize a new ConfigMap + data = map[string]interface{}{ + "metadata": map[string]interface{}{ + "name": configMapName, + "namespace": namespace, + }, + "data": map[string]interface{}{}, } + } else { + data = cm.Object } - // Append the new entry + // Retrieve existing data and append the new entry + existingData := data["data"].(map[string]interface{}) + var entries []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) + } + } entries = append(entries, entry) - // Write the updated entries back to the file - data, err := yamlv3.Marshal(entries) + // Marshal the updated data back to YAML + updatedData, err := yamling.Marshal(entries) if err != nil { - return fmt.Errorf("failed to marshal installer data: %w", err) + return fmt.Errorf("failed to marshal updated data: %v", err) } - if err := os.WriteFile(filePath, data, 0644); err != nil { - return fmt.Errorf("failed to write installer file: %w", err) + // Update the ConfigMap data + existingData[dataKey] = string(updatedData) + data["data"] = existingData + + // Apply the ConfigMap + if cm == nil { + _, err = dynamicClient.Resource(configMapResource).Namespace(namespace).Create(context.TODO(), &unstructured.Unstructured{ + Object: data, + }, metav1.CreateOptions{}) + if err != nil { + return fmt.Errorf("failed to create ConfigMap: %v", err) + } + } else { + _, err = dynamicClient.Resource(configMapResource).Namespace(namespace).Update(context.TODO(), &unstructured.Unstructured{ + Object: data, + }, metav1.UpdateOptions{}) + if err != nil { + return fmt.Errorf("failed to update ConfigMap: %v", err) + } } return nil diff --git a/pkg/installer/model.go b/pkg/installer/model.go index b9f3572..cbb09a6 100644 --- a/pkg/installer/model.go +++ b/pkg/installer/model.go @@ -99,6 +99,5 @@ type InstallerEntry struct { Name string `yaml:"name"` Namespace string `yaml:"namespace"` Version string `yaml:"version"` - Context string `yaml:"context"` Status string `yaml:"status"` // todo ideally should be a heartbeat } diff --git a/pkg/installer/uninstaller.go b/pkg/installer/uninstaller.go index 47a3816..005d8d5 100644 --- a/pkg/installer/uninstaller.go +++ b/pkg/installer/uninstaller.go @@ -57,7 +57,7 @@ func Uninstaller(verbose bool) error { // Prompt the user to select a cluster clusterNames := make([]string, len(entries)) for i, entry := range entries { - clusterNames[i] = fmt.Sprintf("[Name: %s] [Namespace: %s] [Context: %s]", entry.Name, entry.Namespace, entry.Context) + clusterNames[i] = fmt.Sprintf("[Name: %s] [Namespace: %s] [Context: %s]", entry.Name, entry.Namespace) } promptClusterSelect := promptui.Select{ From 1f22051d0fd4321ab02b4e09c3cafb3a123f8ee6 Mon Sep 17 00:00:00 2001 From: AdheipSingh Date: Sun, 22 Dec 2024 08:06:15 +0530 Subject: [PATCH 06/13] 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 From 1988da9d6573c8ab5666108546d68e1d75a7497f Mon Sep 17 00:00:00 2001 From: AdheipSingh Date: Sun, 22 Dec 2024 09:30:18 +0530 Subject: [PATCH 07/13] show values, uninstall, list --- cmd/list.go | 14 +++++++++ cmd/show.go | 75 ++++++++++++++++++++++++++++++++++++++++++++ cmd/uninstaller.go | 44 ++++++++++++++++++++++++-- main.go | 20 ++++++++++++ pkg/common/common.go | 69 +++++++++++++++++++++++++++++++++++++--- pkg/helm/helm.go | 19 +++-------- 6 files changed, 220 insertions(+), 21 deletions(-) create mode 100644 cmd/show.go diff --git a/cmd/list.go b/cmd/list.go index 4830138..83dc796 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -1,3 +1,17 @@ +// 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 ( diff --git a/cmd/show.go b/cmd/show.go new file mode 100644 index 0000000..1ad41ef --- /dev/null +++ b/cmd/show.go @@ -0,0 +1,75 @@ +// 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/helm" + + "github.com/spf13/cobra" + "gopkg.in/yaml.v2" +) + +// ShowValuesCmd lists the Parseable OSS servers +var ShowValuesCmd = &cobra.Command{ + Use: "values", + Short: "Show values available in Parseable OSS servers", + Example: "pb show values", + 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 list OSS servers: %v", err) + } + + // Check if there are no entries + if len(entries) == 0 { + fmt.Println("No OSS servers found.") + return + } + + // Prompt user to select a cluster + selectedCluster, err := common.PromptClusterSelection(entries) + if err != nil { + log.Fatalf("Failed to select a cluster: %v", err) + } + + values, err := helm.GetReleaseValues(selectedCluster.Name, selectedCluster.Namespace) + if err != nil { + log.Fatalf("Failed to get values for release: %v", err) + } + + // Marshal values to YAML for nice formatting + yamlOutput, err := yaml.Marshal(values) + if err != nil { + log.Fatalf("Failed to marshal values to YAML: %v", err) + } + + // Print the YAML output + fmt.Println(string(yamlOutput)) + + // Print instructions for fetching secret values + fmt.Printf("\nTo get secret values of the Parseable cluster, run the following command:\n") + fmt.Printf("kubectl get secret -n %s parseable-env-secret -o jsonpath='{.data}' | jq -r 'to_entries[] | \"\\(.key): \\(.value | @base64d)\"'\n", selectedCluster.Namespace) + }, +} diff --git a/cmd/uninstaller.go b/cmd/uninstaller.go index 0d770b8..4fb1fb4 100644 --- a/cmd/uninstaller.go +++ b/cmd/uninstaller.go @@ -1,6 +1,7 @@ package cmd import ( + "context" "fmt" "log" @@ -8,6 +9,8 @@ import ( "pb/pkg/helm" "github.com/spf13/cobra" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" ) // UninstallOssCmd removes Parseable OSS servers @@ -39,6 +42,12 @@ var UninstallOssCmd = &cobra.Command{ log.Fatalf("Failed to select a cluster: %v", err) } + // Display a warning banner + fmt.Println("\n────────────────────────────────────────────────────────────────────────────") + fmt.Println("āš ļø Deleting this cluster will not delete any data on object storage.") + fmt.Println(" This operation will clean up the Parseable deployment on Kubernetes.") + fmt.Println("────────────────────────────────────────────────────────────────────────────") + // 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)) { @@ -47,8 +56,20 @@ var UninstallOssCmd = &cobra.Command{ } // Perform uninstallation - if err := uninstallCluster(selectedCluster); err != nil { - log.Fatalf("Failed to uninstall cluster: %v", err) + // if err := uninstallCluster(selectedCluster); err != nil { + // log.Fatalf("Failed to uninstall cluster: %v", err) + // } + + // Remove entry from ConfigMap + if err := common.RemoveInstallerEntry(selectedCluster.Name); err != nil { + log.Fatalf("Failed to remove entry from ConfigMap: %v", err) + } + + // Delete secret + if err := deleteSecret(selectedCluster.Namespace, "parseable-env-secret"); err != nil { + log.Printf("Warning: Failed to delete secret 'parseable-env-secret': %v", err) + } else { + fmt.Println(common.Green + "Secret 'parseable-env-secret' deleted successfully." + common.Reset) } fmt.Println(common.Green + "Uninstallation completed successfully." + common.Reset) @@ -80,3 +101,22 @@ func uninstallCluster(entry common.InstallerEntry) error { fmt.Printf(common.Green+"Successfully uninstalled '%s' from namespace '%s'.\n"+common.Reset, entry.Name, entry.Namespace) return nil } + +func deleteSecret(namespace, secretName string) error { + config, err := common.LoadKubeConfig() + if err != nil { + return fmt.Errorf("failed to create Kubernetes client: %v", err) + } + + clientset, err := kubernetes.NewForConfig(config) + if err != nil { + return fmt.Errorf("failed to create Kubernetes client: %w", err) + } + + err = clientset.CoreV1().Secrets(namespace).Delete(context.TODO(), "parseable-env-secret", metav1.DeleteOptions{}) + if err != nil { + return fmt.Errorf("failed to delete secret '%s': %v", secretName, err) + } + + return nil +} diff --git a/main.go b/main.go index a6bb0c5..5b4ef04 100644 --- a/main.go +++ b/main.go @@ -220,6 +220,23 @@ var list = &cobra.Command{ }, } +var show = &cobra.Command{ + Use: "show", + Short: "Show outputs values defined when installing parseable on kubernetes cluster", + Long: "\nshow command is used to get values in parseable.", + 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, "install", args) + }() + }, +} + var uninstall = &cobra.Command{ Use: "uninstall", Short: "Uninstall parseable on kubernetes cluster", @@ -269,6 +286,8 @@ func main() { uninstall.AddCommand(pb.UninstallOssCmd) + show.AddCommand(pb.ShowValuesCmd) + cli.AddCommand(profile) cli.AddCommand(query) cli.AddCommand(stream) @@ -281,6 +300,7 @@ func main() { cli.AddCommand(uninstall) cli.AddCommand(schema) cli.AddCommand(list) + cli.AddCommand(show) // Set as command pb.VersionCmd.Run = func(_ *cobra.Command, _ []string) { diff --git a/pkg/common/common.go b/pkg/common/common.go index 0fe5b13..c61131e 100644 --- a/pkg/common/common.go +++ b/pkg/common/common.go @@ -31,6 +31,12 @@ import ( "k8s.io/client-go/tools/clientcmd" ) +const ( + configMapName = "parseable-installer" + namespace = "pb-system" + dataKey = "installer-data" +) + // ANSI escape codes for colors const ( Yellow = "\033[33m" @@ -51,11 +57,6 @@ type InstallerEntry struct { // 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() @@ -200,3 +201,61 @@ func CreateDeploymentSpinner(namespace, infoMsg string) *spinner.Spinner { return s } +func RemoveInstallerEntry(name string) error { + // Load kubeconfig and create a Kubernetes client + config, err := LoadKubeConfig() + if err != nil { + return fmt.Errorf("failed to load kubeconfig: %w", err) + } + + clientset, err := kubernetes.NewForConfig(config) + if err != nil { + return fmt.Errorf("failed to create Kubernetes client: %w", err) + } + + // Fetch the ConfigMap + configMap, err := clientset.CoreV1().ConfigMaps(namespace).Get(context.TODO(), configMapName, metav1.GetOptions{}) + if err != nil { + return fmt.Errorf("failed to fetch ConfigMap: %v", err) + } + + // Log the current data in the ConfigMap + + // Assuming the entries are stored as YAML or JSON string, unmarshal them into a slice + var entries []map[string]interface{} + if err := yaml.Unmarshal([]byte(configMap.Data["installer-data"]), &entries); err != nil { + return fmt.Errorf("failed to unmarshal installer data: %w", err) + } + + // Find the entry to remove by name + var indexToRemove = -1 + for i, entry := range entries { + if entry["name"] == name { + indexToRemove = i + break + } + } + + // Check if the entry was found + if indexToRemove == -1 { + return fmt.Errorf("entry '%s' does not exist in ConfigMap", name) + } + + // Remove the entry + entries = append(entries[:indexToRemove], entries[indexToRemove+1:]...) + + // Marshal the updated entries back into YAML + updatedData, err := yaml.Marshal(entries) + if err != nil { + return fmt.Errorf("failed to marshal updated entries: %w", err) + } + configMap.Data["installer-data"] = string(updatedData) + + // Update the ConfigMap in Kubernetes + _, err = clientset.CoreV1().ConfigMaps(namespace).Update(context.TODO(), configMap, metav1.UpdateOptions{}) + if err != nil { + return fmt.Errorf("failed to update ConfigMap: %v", err) + } + + return nil +} diff --git a/pkg/helm/helm.go b/pkg/helm/helm.go index 91928fa..ea52248 100644 --- a/pkg/helm/helm.go +++ b/pkg/helm/helm.go @@ -257,7 +257,7 @@ func ListRelease(releaseName, namespace string) (bool, error) { return false, nil } -func GetReleaseValues(chartName, namespace string) (map[string]interface{}, error) { +func GetReleaseValues(releaseName, namespace string) (map[string]interface{}, error) { settings := cli.New() // Initialize action configuration @@ -266,24 +266,15 @@ func GetReleaseValues(chartName, namespace string) (map[string]interface{}, erro return nil, err } - // Create a new List action - client := action.NewList(actionConfig) + // Create a new get action + client := action.NewGet(actionConfig) - // Run the List action to get releases - releases, err := client.Run() + release, err := client.Run(releaseName) if err != nil { return nil, err } - // Iterate over the releases - for _, release := range releases { - // Check if the release's chart name matches the specified chart name - if release.Chart.Name() == chartName { - return release.Chart.Values, nil - } - } - - return nil, nil + return release.Config, nil } // DeleteRelease deletes a Helm release based on the specified chart name and namespace. From c1efcd65052e40214468ce3e1b2c6d465ae81c85 Mon Sep 17 00:00:00 2001 From: AdheipSingh Date: Sun, 22 Dec 2024 09:45:24 +0530 Subject: [PATCH 08/13] lint checks --- cmd/list.go | 5 +++-- cmd/show.go | 5 +++-- cmd/uninstaller.go | 14 +++++++------- pkg/common/common.go | 6 +++--- pkg/installer/installer.go | 32 ++++++++++++++++---------------- pkg/installer/model.go | 10 ---------- pkg/installer/uninstaller.go | 10 ++-------- 7 files changed, 34 insertions(+), 48 deletions(-) diff --git a/cmd/list.go b/cmd/list.go index 83dc796..afd62c6 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -1,3 +1,5 @@ +package cmd + // Copyright (c) 2024 Parseable, Inc // // This program is free software: you can redistribute it and/or modify @@ -12,7 +14,6 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -package cmd import ( "fmt" @@ -30,7 +31,7 @@ var ListOssCmd = &cobra.Command{ Use: "oss", Short: "List available Parseable OSS servers", Example: "pb list oss", - Run: func(cmd *cobra.Command, _ []string) { + Run: func(_ *cobra.Command, _ []string) { _, err := common.PromptK8sContext() if err != nil { log.Fatalf("Failed to prompt for kubernetes context: %v", err) diff --git a/cmd/show.go b/cmd/show.go index 1ad41ef..84afc5c 100644 --- a/cmd/show.go +++ b/cmd/show.go @@ -1,3 +1,5 @@ +package cmd + // Copyright (c) 2024 Parseable, Inc // // This program is free software: you can redistribute it and/or modify @@ -12,7 +14,6 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -package cmd import ( "fmt" @@ -30,7 +31,7 @@ var ShowValuesCmd = &cobra.Command{ Use: "values", Short: "Show values available in Parseable OSS servers", Example: "pb show values", - Run: func(cmd *cobra.Command, _ []string) { + Run: func(_ *cobra.Command, _ []string) { _, err := common.PromptK8sContext() if err != nil { log.Fatalf("Failed to prompt for Kubernetes context: %v", err) diff --git a/cmd/uninstaller.go b/cmd/uninstaller.go index 4fb1fb4..a7d4092 100644 --- a/cmd/uninstaller.go +++ b/cmd/uninstaller.go @@ -18,7 +18,7 @@ var UninstallOssCmd = &cobra.Command{ Use: "oss", Short: "Uninstall Parseable OSS servers", Example: "pb uninstall oss", - Run: func(cmd *cobra.Command, _ []string) { + Run: func(_ *cobra.Command, _ []string) { _, err := common.PromptK8sContext() if err != nil { log.Fatalf("Failed to prompt for Kubernetes context: %v", err) @@ -32,7 +32,7 @@ var UninstallOssCmd = &cobra.Command{ // Check if there are no entries if len(entries) == 0 { - fmt.Println(common.Yellow + "\nNo Parseable OSS servers found to uninstall.\n") + fmt.Println(common.Yellow + "\nNo Parseable OSS servers found to uninstall.") return } @@ -55,10 +55,10 @@ var UninstallOssCmd = &cobra.Command{ return } - // Perform uninstallation - // if err := uninstallCluster(selectedCluster); err != nil { - // log.Fatalf("Failed to uninstall cluster: %v", err) - // } + //Perform uninstallation + if err := uninstallCluster(selectedCluster); err != nil { + log.Fatalf("Failed to uninstall cluster: %v", err) + } // Remove entry from ConfigMap if err := common.RemoveInstallerEntry(selectedCluster.Name); err != nil { @@ -88,7 +88,7 @@ func uninstallCluster(entry common.InstallerEntry) error { fmt.Println(common.Yellow + "Starting uninstallation process..." + common.Reset) - spinner := common.CreateDeploymentSpinner(entry.Namespace, fmt.Sprintf("Uninstalling Parseable OSS '%s'...", entry.Name)) + spinner := common.CreateDeploymentSpinner(fmt.Sprintf("Uninstalling Parseable OSS '%s'...", entry.Name)) spinner.Start() _, err := helm.Uninstall(helmApp, false) diff --git a/pkg/common/common.go b/pkg/common/common.go index c61131e..8f74354 100644 --- a/pkg/common/common.go +++ b/pkg/common/common.go @@ -84,7 +84,7 @@ func ReadInstallerConfigMap() ([]InstallerEntry, error) { 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") + fmt.Println(Yellow + "────────────────────────────────────────────────────────────────────────────") return nil, nil } @@ -184,7 +184,7 @@ func PromptConfirmation(message string) bool { return err == nil } -func CreateDeploymentSpinner(namespace, infoMsg string) *spinner.Spinner { +func CreateDeploymentSpinner(infoMsg string) *spinner.Spinner { // Custom spinner with multiple character sets for dynamic effect spinnerChars := []string{ "ā—", "ā—‹", "ā—‰", "ā—‹", "ā—‰", "ā—‹", "ā—‰", "ā—‹", "ā—‰", @@ -197,7 +197,7 @@ func CreateDeploymentSpinner(namespace, infoMsg string) *spinner.Spinner { spinner.WithSuffix(" ..."), ) - s.Prefix = fmt.Sprintf(Yellow + infoMsg) + s.Prefix = Yellow + infoMsg return s } diff --git a/pkg/installer/installer.go b/pkg/installer/installer.go index e85354f..3f673ad 100644 --- a/pkg/installer/installer.go +++ b/pkg/installer/installer.go @@ -78,7 +78,7 @@ func waterFall(verbose bool) { } // Prompt for agent deployment - _, agentValues, err := promptAgentDeployment(chartValues, distributed, *pbInfo) + _, agentValues, err := promptAgentDeployment(chartValues, *pbInfo) if err != nil { log.Fatalf("Failed to prompt for agent deployment: %v", err) } @@ -372,7 +372,7 @@ data: } // promptAgentDeployment prompts the user for agent deployment options -func promptAgentDeployment(chartValues []string, deployment deploymentType, pbInfo ParseableInfo) (string, []string, error) { +func promptAgentDeployment(chartValues []string, pbInfo ParseableInfo) (string, []string, error) { // Prompt for Agent Deployment type promptAgentSelect := promptui.Select{ Items: []string{string(fluentbit), string(vector), "I have my agent running / I'll set up later"}, @@ -388,10 +388,10 @@ func promptAgentDeployment(chartValues []string, deployment deploymentType, pbIn return "", nil, fmt.Errorf("failed to prompt for agent deployment type: %w", err) } - ingestorUrl, _ := getParseableSvcUrls(pbInfo.Name, pbInfo.Namespace) + ingestorURL, _ := getParseableSvcUrls(pbInfo.Name, pbInfo.Namespace) if agentDeploymentType == string(fluentbit) { - chartValues = append(chartValues, "fluent-bit.serverHost="+ingestorUrl) + chartValues = append(chartValues, "fluent-bit.serverHost="+ingestorURL) chartValues = append(chartValues, "fluent-bit.serverUsername="+pbInfo.Username) chartValues = append(chartValues, "fluent-bit.serverPassword="+pbInfo.Password) chartValues = append(chartValues, "fluent-bit.serverStream="+"$NAMESPACE") @@ -672,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 := common.CreateDeploymentSpinner(config.Namespace, msg) + spinner := common.CreateDeploymentSpinner(msg) // Redirect standard output if not in verbose mode var oldStdout *os.File @@ -716,7 +716,7 @@ func deployRelease(config HelmDeploymentConfig) error { // printSuccessBanner remains the same as in the original code func printSuccessBanner(pbInfo ParseableInfo, version string) { - ingestionUrl, queryUrl := getParseableSvcUrls(pbInfo.Name, pbInfo.Namespace) + ingestorURL, queryURL := getParseableSvcUrls(pbInfo.Name, pbInfo.Namespace) // Encode credentials to Base64 credentials := map[string]string{ @@ -737,7 +737,7 @@ func printSuccessBanner(pbInfo ParseableInfo, version string) { fmt.Printf("%s Deployment Details:\n", common.Blue+"ā„¹ļø ") fmt.Printf(" • Namespace: %s\n", common.Blue+pbInfo.Namespace) fmt.Printf(" • Chart Version: %s\n", common.Blue+version) - fmt.Printf(" • Ingestion URL: %s\n", ingestionUrl) + fmt.Printf(" • Ingestion URL: %s\n", ingestorURL) fmt.Println("\n" + common.Blue + "šŸ”— Resources:" + common.Reset) fmt.Println(common.Blue + " • Documentation: https://www.parseable.com/docs/server/introduction") @@ -747,9 +747,9 @@ func printSuccessBanner(pbInfo ParseableInfo, version string) { // Port-forward the service localPort := "8001" - fmt.Printf(common.Green+"Port-forwarding %s service on port %s in namespace %s...\n"+common.Reset, queryUrl, localPort, pbInfo.Namespace) + fmt.Printf(common.Green+"Port-forwarding %s service on port %s in namespace %s...\n"+common.Reset, queryURL, localPort, pbInfo.Namespace) - if err = startPortForward(pbInfo.Namespace, queryUrl, "80", localPort, false); err != nil { + if err = startPortForward(pbInfo.Namespace, queryURL, "80", localPort, false); err != nil { fmt.Printf(common.Red+"failed to port-forward service: %s", err.Error()) } @@ -927,13 +927,13 @@ func updateInstallerConfigMap(entry common.InstallerEntry) error { return nil } -func getParseableSvcUrls(releaseName, namespace string) (ingestorUrl, queryUrl string) { +func getParseableSvcUrls(releaseName, namespace string) (ingestorURL, queryURL string) { if releaseName == "parseable" { - ingestorUrl = releaseName + "-ingestor-service." + namespace + ".svc.cluster.local" - queryUrl = releaseName + "-querier-service" - return ingestorUrl, queryUrl + ingestorURL = releaseName + "-ingestor-service." + namespace + ".svc.cluster.local" + queryURL = releaseName + "-querier-service" + return ingestorURL, queryURL } - ingestorUrl = releaseName + "-parseable-ingestor-service." + namespace + ".svc.cluster.local" - queryUrl = releaseName + "-parseable-querier-service" - return ingestorUrl, queryUrl + ingestorURL = releaseName + "-parseable-ingestor-service." + namespace + ".svc.cluster.local" + queryURL = releaseName + "-parseable-querier-service" + return ingestorURL, queryURL } diff --git a/pkg/installer/model.go b/pkg/installer/model.go index 84d08c4..32a3ce4 100644 --- a/pkg/installer/model.go +++ b/pkg/installer/model.go @@ -15,16 +15,6 @@ package installer -// deploymentType represents the type of deployment for the application. -type deploymentType string - -const ( - // standalone is a single-node deployment. - standalone deploymentType = "standalone" - // distributed is a multi-node deployment. - distributed deploymentType = "distributed" -) - // loggingAgent represents the type of logging agent used. type loggingAgent string diff --git a/pkg/installer/uninstaller.go b/pkg/installer/uninstaller.go index ea9ef6e..ddb9d86 100644 --- a/pkg/installer/uninstaller.go +++ b/pkg/installer/uninstaller.go @@ -57,7 +57,7 @@ func Uninstaller(verbose bool) error { // Prompt the user to select a cluster clusterNames := make([]string, len(entries)) for i, entry := range entries { - clusterNames[i] = fmt.Sprintf("[Name: %s] [Namespace: %s] [Context: %s]", entry.Name, entry.Namespace) + clusterNames[i] = fmt.Sprintf("[Name: %s] [Namespace: %s]", entry.Name, entry.Namespace) } promptClusterSelect := promptui.Select{ @@ -78,12 +78,6 @@ func Uninstaller(verbose bool) error { selectedCluster := entries[index] - // Display a warning banner - fmt.Println("\n────────────────────────────────────────────────────────────────────────────") - fmt.Println("āš ļø Deleting this cluster will not delete any data on object storage.") - fmt.Println(" This operation will clean up the Parseable deployment on Kubernetes.") - fmt.Println("────────────────────────────────────────────────────────────────────────────") - // Confirm deletion confirm, err := promptUserConfirmation(fmt.Sprintf(common.Yellow+"Do you still want to proceed with deleting the cluster '%s'?", selectedCluster.Name)) if err != nil { @@ -105,7 +99,7 @@ func Uninstaller(verbose bool) error { } // Create a spinner - spinner := common.CreateDeploymentSpinner(selectedCluster.Namespace, "Uninstalling Parseable in ") + spinner := common.CreateDeploymentSpinner("Uninstalling Parseable in ") // Redirect standard output if not in verbose mode var oldStdout *os.File From a836af2e7168b4b99652df79b7b30ab7f13d7761 Mon Sep 17 00:00:00 2001 From: AdheipSingh Date: Sun, 22 Dec 2024 18:07:36 +0530 Subject: [PATCH 09/13] move installer cmds with cluster prefix --- cmd/{uninstaller.go => cluster.go} | 119 ++++++++++++++++++++++++++++- cmd/installer.go | 35 --------- cmd/list.go | 62 --------------- cmd/show.go | 76 ------------------ main.go | 28 +++++-- 5 files changed, 139 insertions(+), 181 deletions(-) rename cmd/{uninstaller.go => cluster.go} (53%) delete mode 100644 cmd/installer.go delete mode 100644 cmd/list.go delete mode 100644 cmd/show.go diff --git a/cmd/uninstaller.go b/cmd/cluster.go similarity index 53% rename from cmd/uninstaller.go rename to cmd/cluster.go index a7d4092..69bbfdf 100644 --- a/cmd/uninstaller.go +++ b/cmd/cluster.go @@ -1,21 +1,136 @@ +// 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 ( "context" "fmt" "log" - + "os" "pb/pkg/common" "pb/pkg/helm" + "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" ) +var verbose bool + +var InstallOssCmd = &cobra.Command{ + Use: "install oss", + Short: "Deploy Parseable OSS", + Example: "pb cluster install oss", + Run: func(cmd *cobra.Command, _ []string) { + // Add verbose flag + cmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Enable verbose logging") + installer.Installer(verbose) + }, +} + +// ListOssCmd lists the Parseable OSS servers +var ListOssCmd = &cobra.Command{ + Use: "list oss", + Short: "List available Parseable OSS servers", + Example: "pb list oss", + Run: func(_ *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 list OSS servers: %v", err) + } + + // Check if there are no entries + if len(entries) == 0 { + fmt.Println("No OSS servers found.") + return + } + + // Display the entries in a table format + table := tablewriter.NewWriter(os.Stdout) + table.SetHeader([]string{"Name", "Namespace", "Version", "Status"}) + + for _, entry := range entries { + table.Append([]string{entry.Name, entry.Namespace, entry.Version, entry.Status}) + } + + table.Render() + }, +} + +// ShowValuesCmd lists the Parseable OSS servers +var ShowValuesCmd = &cobra.Command{ + Use: "show values oss", + Short: "Show values available in Parseable OSS servers", + Example: "pb show values", + Run: func(_ *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 list OSS servers: %v", err) + } + + // Check if there are no entries + if len(entries) == 0 { + fmt.Println("No OSS servers found.") + return + } + + // Prompt user to select a cluster + selectedCluster, err := common.PromptClusterSelection(entries) + if err != nil { + log.Fatalf("Failed to select a cluster: %v", err) + } + + values, err := helm.GetReleaseValues(selectedCluster.Name, selectedCluster.Namespace) + if err != nil { + log.Fatalf("Failed to get values for release: %v", err) + } + + // Marshal values to YAML for nice formatting + yamlOutput, err := yaml.Marshal(values) + if err != nil { + log.Fatalf("Failed to marshal values to YAML: %v", err) + } + + // Print the YAML output + fmt.Println(string(yamlOutput)) + + // Print instructions for fetching secret values + fmt.Printf("\nTo get secret values of the Parseable cluster, run the following command:\n") + fmt.Printf("kubectl get secret -n %s parseable-env-secret -o jsonpath='{.data}' | jq -r 'to_entries[] | \"\\(.key): \\(.value | @base64d)\"'\n", selectedCluster.Namespace) + }, +} + // UninstallOssCmd removes Parseable OSS servers var UninstallOssCmd = &cobra.Command{ - Use: "oss", + Use: "uninstall oss", Short: "Uninstall Parseable OSS servers", Example: "pb uninstall oss", Run: func(_ *cobra.Command, _ []string) { diff --git a/cmd/installer.go b/cmd/installer.go deleted file mode 100644 index 9c86652..0000000 --- a/cmd/installer.go +++ /dev/null @@ -1,35 +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 cmd - -import ( - "pb/pkg/installer" - - "github.com/spf13/cobra" -) - -var verbose bool - -var InstallOssCmd = &cobra.Command{ - Use: "oss", - Short: "Deploy Parseable OSS", - Example: "pb install oss", - Run: func(cmd *cobra.Command, _ []string) { - // Add verbose flag - cmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Enable verbose logging") - installer.Installer(verbose) - }, -} diff --git a/cmd/list.go b/cmd/list.go deleted file mode 100644 index afd62c6..0000000 --- a/cmd/list.go +++ /dev/null @@ -1,62 +0,0 @@ -package cmd - -// 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 . - -import ( - "fmt" - "log" - "os" - - "pb/pkg/common" - - "github.com/olekukonko/tablewriter" - "github.com/spf13/cobra" -) - -// ListOssCmd lists the Parseable OSS servers -var ListOssCmd = &cobra.Command{ - Use: "oss", - Short: "List available Parseable OSS servers", - Example: "pb list oss", - Run: func(_ *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 list OSS servers: %v", err) - } - - // Check if there are no entries - if len(entries) == 0 { - fmt.Println("No OSS servers found.") - return - } - - // Display the entries in a table format - table := tablewriter.NewWriter(os.Stdout) - table.SetHeader([]string{"Name", "Namespace", "Version", "Status"}) - - for _, entry := range entries { - table.Append([]string{entry.Name, entry.Namespace, entry.Version, entry.Status}) - } - - table.Render() - }, -} diff --git a/cmd/show.go b/cmd/show.go deleted file mode 100644 index 84afc5c..0000000 --- a/cmd/show.go +++ /dev/null @@ -1,76 +0,0 @@ -package cmd - -// 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 . - -import ( - "fmt" - "log" - - "pb/pkg/common" - "pb/pkg/helm" - - "github.com/spf13/cobra" - "gopkg.in/yaml.v2" -) - -// ShowValuesCmd lists the Parseable OSS servers -var ShowValuesCmd = &cobra.Command{ - Use: "values", - Short: "Show values available in Parseable OSS servers", - Example: "pb show values", - Run: func(_ *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 list OSS servers: %v", err) - } - - // Check if there are no entries - if len(entries) == 0 { - fmt.Println("No OSS servers found.") - return - } - - // Prompt user to select a cluster - selectedCluster, err := common.PromptClusterSelection(entries) - if err != nil { - log.Fatalf("Failed to select a cluster: %v", err) - } - - values, err := helm.GetReleaseValues(selectedCluster.Name, selectedCluster.Namespace) - if err != nil { - log.Fatalf("Failed to get values for release: %v", err) - } - - // Marshal values to YAML for nice formatting - yamlOutput, err := yaml.Marshal(values) - if err != nil { - log.Fatalf("Failed to marshal values to YAML: %v", err) - } - - // Print the YAML output - fmt.Println(string(yamlOutput)) - - // Print instructions for fetching secret values - fmt.Printf("\nTo get secret values of the Parseable cluster, run the following command:\n") - fmt.Printf("kubectl get secret -n %s parseable-env-secret -o jsonpath='{.data}' | jq -r 'to_entries[] | \"\\(.key): \\(.value | @base64d)\"'\n", selectedCluster.Namespace) - }, -} diff --git a/main.go b/main.go index 5b4ef04..3a44b3e 100644 --- a/main.go +++ b/main.go @@ -203,6 +203,23 @@ var install = &cobra.Command{ }, } +var cluster = &cobra.Command{ + Use: "cluster", + Short: "Cluster operations for parseable.", + Long: "\nCluster operations for parseable cluster on kubernetes.", + 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, "install", args) + }() + }, +} + var list = &cobra.Command{ Use: "list", Short: "List parseable on kubernetes cluster", @@ -280,7 +297,10 @@ func main() { schema.AddCommand(pb.GenerateSchemaCmd) schema.AddCommand(pb.CreateSchemaCmd) - install.AddCommand(pb.InstallOssCmd) + cluster.AddCommand(pb.InstallOssCmd) + cluster.AddCommand(pb.ListOssCmd) + cluster.AddCommand(pb.ShowValuesCmd) + cluster.AddCommand(pb.UninstallOssCmd) list.AddCommand(pb.ListOssCmd) @@ -294,13 +314,9 @@ func main() { cli.AddCommand(user) cli.AddCommand(role) cli.AddCommand(pb.TailCmd) + cli.AddCommand(cluster) cli.AddCommand(pb.AutocompleteCmd) - cli.AddCommand(install) - cli.AddCommand(uninstall) - cli.AddCommand(schema) - cli.AddCommand(list) - cli.AddCommand(show) // Set as command pb.VersionCmd.Run = func(_ *cobra.Command, _ []string) { From 0c348171074ef92a00cf19c1e37ceb0c4d4b7086 Mon Sep 17 00:00:00 2001 From: AdheipSingh Date: Sun, 22 Dec 2024 18:20:29 +0530 Subject: [PATCH 10/13] remove oss --- cmd/cluster.go | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/cmd/cluster.go b/cmd/cluster.go index 69bbfdf..99bacfd 100644 --- a/cmd/cluster.go +++ b/cmd/cluster.go @@ -34,9 +34,9 @@ import ( var verbose bool var InstallOssCmd = &cobra.Command{ - Use: "install oss", - Short: "Deploy Parseable OSS", - Example: "pb cluster install oss", + Use: "install", + Short: "Deploy Parseable", + Example: "pb cluster install", Run: func(cmd *cobra.Command, _ []string) { // Add verbose flag cmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Enable verbose logging") @@ -46,9 +46,9 @@ var InstallOssCmd = &cobra.Command{ // ListOssCmd lists the Parseable OSS servers var ListOssCmd = &cobra.Command{ - Use: "list oss", - Short: "List available Parseable OSS servers", - Example: "pb list oss", + Use: "list", + Short: "List available Parseable servers", + Example: "pb list", Run: func(_ *cobra.Command, _ []string) { _, err := common.PromptK8sContext() if err != nil { @@ -58,12 +58,12 @@ var ListOssCmd = &cobra.Command{ // Read the installer data from the ConfigMap entries, err := common.ReadInstallerConfigMap() if err != nil { - log.Fatalf("Failed to list OSS servers: %v", err) + log.Fatalf("Failed to list servers: %v", err) } // Check if there are no entries if len(entries) == 0 { - fmt.Println("No OSS servers found.") + fmt.Println("No clusters found.") return } @@ -81,8 +81,8 @@ var ListOssCmd = &cobra.Command{ // ShowValuesCmd lists the Parseable OSS servers var ShowValuesCmd = &cobra.Command{ - Use: "show values oss", - Short: "Show values available in Parseable OSS servers", + Use: "show values", + Short: "Show values available in Parseable servers", Example: "pb show values", Run: func(_ *cobra.Command, _ []string) { _, err := common.PromptK8sContext() @@ -130,9 +130,9 @@ var ShowValuesCmd = &cobra.Command{ // UninstallOssCmd removes Parseable OSS servers var UninstallOssCmd = &cobra.Command{ - Use: "uninstall oss", - Short: "Uninstall Parseable OSS servers", - Example: "pb uninstall oss", + Use: "uninstall", + Short: "Uninstall Parseable servers", + Example: "pb uninstall", Run: func(_ *cobra.Command, _ []string) { _, err := common.PromptK8sContext() if err != nil { From 437576f213efd04cc81883f0a1ef0c98b4334e38 Mon Sep 17 00:00:00 2001 From: AdheipSingh Date: Mon, 23 Dec 2024 10:37:35 +0530 Subject: [PATCH 11/13] tests for azure, aws and gcs --- pkg/installer/installer.go | 68 +++++++++++++++++++++++++++----------- pkg/installer/model.go | 11 +++--- pkg/installer/plans.go | 20 ++++++----- 3 files changed, 67 insertions(+), 32 deletions(-) diff --git a/pkg/installer/installer.go b/pkg/installer/installer.go index 3f673ad..55f257c 100644 --- a/pkg/installer/installer.go +++ b/pkg/installer/installer.go @@ -111,6 +111,10 @@ func waterFall(verbose bool) { Verbose: verbose, } + if err := deployRelease(config); err != nil { + log.Fatalf("Failed to deploy parseable, err: %v", err) + } + if err := updateInstallerConfigMap(common.InstallerEntry{ Name: pbInfo.Name, Namespace: pbInfo.Namespace, @@ -119,9 +123,7 @@ func waterFall(verbose bool) { }); err != nil { log.Fatalf("Failed to update parseable installer file, err: %v", err) } - if err := deployRelease(config); err != nil { - log.Fatalf("Failed to deploy parseable, err: %v", err) - } + printSuccessBanner(*pbInfo, config.Version) } @@ -228,6 +230,8 @@ func applyParseableSecret(ps *ParseableInfo, store ObjectStore, objectStoreConfi secretManifest = getParseableSecretGcs(ps, objectStoreConfig) } + fmt.Println(secretManifest) + // apply the Kubernetes Secret if err := applyManifest(secretManifest); err != nil { return fmt.Errorf("failed to create and apply secret: %w", err) @@ -247,7 +251,6 @@ metadata: namespace: %s type: Opaque data: -- addr azr.access_key: %s azr.account: %s azr.container: %s @@ -260,7 +263,7 @@ data: `, ps.Namespace, base64.StdEncoding.EncodeToString([]byte(objectStore.BlobStore.AccessKey)), - base64.StdEncoding.EncodeToString([]byte(objectStore.BlobStore.AccountName)), + base64.StdEncoding.EncodeToString([]byte(objectStore.BlobStore.StorageAccountName)), base64.StdEncoding.EncodeToString([]byte(objectStore.BlobStore.Container)), base64.StdEncoding.EncodeToString([]byte(objectStore.BlobStore.URL)), base64.StdEncoding.EncodeToString([]byte(ps.Username)), @@ -462,12 +465,18 @@ func promptStoreConfigs(store ObjectStore, chartValues []string, plan Plan) (Obj switch store { case S3Store: storeValues.S3Store = S3{ - URL: promptForInput(common.Yellow + " Enter S3 URL: " + common.Reset), - AccessKey: promptForInput(common.Yellow + " Enter S3 Access Key: " + common.Reset), - SecretKey: promptForInput(common.Yellow + " Enter S3 Secret Key: " + common.Reset), - Bucket: promptForInput(common.Yellow + " Enter S3 Bucket: " + common.Reset), - Region: promptForInput(common.Yellow + " Enter S3 Region: " + common.Reset), + Region: promptForInputWithDefault(common.Yellow+" Enter S3 Region (default: us-east-1): "+common.Reset, "us-east-1"), + AccessKey: promptForInputWithDefault(common.Yellow+" Enter S3 Access Key: "+common.Reset, ""), + SecretKey: promptForInputWithDefault(common.Yellow+" Enter S3 Secret Key: "+common.Reset, ""), + Bucket: promptForInputWithDefault(common.Yellow+" Enter S3 Bucket: "+common.Reset, ""), } + + // Dynamically construct the URL after Region is set + storeValues.S3Store.URL = promptForInputWithDefault( + common.Yellow+" Enter S3 URL (default: https://s3."+storeValues.S3Store.Region+".amazonaws.com): "+common.Reset, + "https://s3."+storeValues.S3Store.Region+".amazonaws.com", + ) + sc, err := promptStorageClass() if err != nil { log.Fatalf("Failed to prompt for storage class: %v", err) @@ -491,9 +500,21 @@ func promptStoreConfigs(store ObjectStore, chartValues []string, plan Plan) (Obj log.Fatalf("Failed to prompt for storage class: %v", err) } storeValues.BlobStore = Blob{ - URL: promptForInput(common.Yellow + " Enter Blob URL: " + common.Reset), - Container: promptForInput(common.Yellow + " Enter Blob Container: " + common.Reset), + StorageAccountName: promptForInputWithDefault(common.Yellow+" Enter Blob Storage Account Name: "+common.Reset, ""), + Container: promptForInputWithDefault(common.Yellow+" Enter Blob Container: "+common.Reset, ""), + // ClientID: promptForInputWithDefault(common.Yellow+" Enter Client ID: "+common.Reset, ""), + // ClientSecret: promptForInputWithDefault(common.Yellow+" Enter Client Secret: "+common.Reset, ""), + // TenantID: promptForInputWithDefault(common.Yellow+" Enter Tenant ID: "+common.Reset, ""), + AccessKey: promptForInputWithDefault(common.Yellow+" Enter Access Keys: "+common.Reset, ""), } + + // Dynamically construct the URL after Region is set + storeValues.BlobStore.URL = promptForInputWithDefault( + common.Yellow+ + " Enter Blob URL (default: https://"+storeValues.BlobStore.StorageAccountName+".blob.core.windows.net): "+ + common.Reset, + "https://"+storeValues.BlobStore.StorageAccountName+".blob.core.windows.net") + storeValues.StorageClass = sc storeValues.ObjectStore = BlobStore chartValues = append(chartValues, "parseable.store="+string(BlobStore)) @@ -512,12 +533,13 @@ func promptStoreConfigs(store ObjectStore, chartValues []string, plan Plan) (Obj log.Fatalf("Failed to prompt for storage class: %v", err) } storeValues.GCSStore = GCS{ - URL: promptForInput(common.Yellow + " Enter GCS URL: " + common.Reset), - AccessKey: promptForInput(common.Yellow + " Enter GCS Access Key: " + common.Reset), - SecretKey: promptForInput(common.Yellow + " Enter GCS Secret Key: " + common.Reset), - Bucket: promptForInput(common.Yellow + " Enter GCS Bucket: " + common.Reset), - Region: promptForInput(common.Yellow + " Enter GCS Region: " + common.Reset), + Bucket: promptForInputWithDefault(common.Yellow+" Enter GCS Bucket: "+common.Reset, ""), + Region: promptForInputWithDefault(common.Yellow+" Enter GCS Region (default: us-east1): "+common.Reset, "us-east1"), + URL: promptForInputWithDefault(common.Yellow+" Enter GCS URL (default: https://storage.googleapis.com):", "https://storage.googleapis.com"), + AccessKey: promptForInputWithDefault(common.Yellow+" Enter GCS Access Key: "+common.Reset, ""), + SecretKey: promptForInputWithDefault(common.Yellow+" Enter GCS Secret Key: "+common.Reset, ""), } + storeValues.StorageClass = sc storeValues.ObjectStore = GcsStore chartValues = append(chartValues, "parseable.store="+string(GcsStore)) @@ -628,12 +650,18 @@ func getGVR(config *rest.Config, obj *unstructured.Unstructured) (schema.GroupVe return mapping.Resource, nil } -// Helper function to prompt for individual input values -func promptForInput(label string) string { +// Helper function to prompt for input with a default value +func promptForInputWithDefault(label, defaultValue string) string { fmt.Print(label) reader := bufio.NewReader(os.Stdin) input, _ := reader.ReadString('\n') - return strings.TrimSpace(input) + input = strings.TrimSpace(input) + + // Use default if input is empty + if input == "" { + return defaultValue + } + return input } // printBanner displays a welcome banner diff --git a/pkg/installer/model.go b/pkg/installer/model.go index 32a3ce4..d7364d0 100644 --- a/pkg/installer/model.go +++ b/pkg/installer/model.go @@ -78,8 +78,11 @@ type GCS struct { // Blob contains configuration details for an Azure Blob Storage backend. type Blob struct { - AccessKey string // Access key for authentication. - AccountName string // Account name for Azure Blob Storage. - Container string // Container name in the Azure Blob store. - URL string // URL of the Azure Blob store. + AccessKey string // Access key for authentication. + StorageAccountName string // Account name for Azure Blob Storage. + Container string // Container name in the Azure Blob store. + ClientID string // Client ID to authenticate. + ClientSecret string // Client Secret to authenticate. + TenantID string // TenantID + URL string // URL of the Azure Blob store. } diff --git a/pkg/installer/plans.go b/pkg/installer/plans.go index f951b62..0fcb6f0 100644 --- a/pkg/installer/plans.go +++ b/pkg/installer/plans.go @@ -80,14 +80,17 @@ func promptUserPlanSelection() (Plan, error) { Details: ` --------- Plan Details ---------- {{ "Plan:" | faint }} {{ .Name }} - {{ "Ingestion Speed:" | faint }} {{ .IngestionSpeed }} - {{ "Per Day Ingestion:" | faint }} {{ .PerDayIngestion }} - {{ "Query Performance:" | faint }} {{ .QueryPerformance }} - {{ "CPU & Memory:" | faint }} {{ .CPUAndMemorySpecs }}`, +{{ "Ingestion Speed:" | faint }} {{ .IngestionSpeed }} +{{ "Per Day Ingestion:" | faint }} {{ .PerDayIngestion }} +{{ "Query Performance:" | faint }} {{ .QueryPerformance }} +{{ "CPU & Memory:" | faint }} {{ .CPUAndMemorySpecs }}`, } + // Add a note about the default plan in the label + label := fmt.Sprintf(common.Yellow + "Select deployment type:") + prompt := promptui.Select{ - Label: fmt.Sprintf(common.Yellow + "Select deployment type"), + Label: label, Items: planList, Templates: templates, } @@ -97,13 +100,14 @@ func promptUserPlanSelection() (Plan, error) { return Plan{}, fmt.Errorf("failed to select deployment type: %w", err) } + selectedPlan := planList[index] fmt.Printf( common.Cyan+" Ingestion Speed: %s\n"+ common.Cyan+" Per Day Ingestion: %s\n"+ common.Cyan+" Query Performance: %s\n"+ common.Cyan+" CPU & Memory: %s\n"+ - common.Reset, planList[index].IngestionSpeed, planList[index].PerDayIngestion, - planList[index].QueryPerformance, planList[index].CPUAndMemorySpecs) + common.Reset, selectedPlan.IngestionSpeed, selectedPlan.PerDayIngestion, + selectedPlan.QueryPerformance, selectedPlan.CPUAndMemorySpecs) - return planList[index], nil + return selectedPlan, nil } From 7b48151aac61070fe1792b47e044e2ce389fa8ba Mon Sep 17 00:00:00 2001 From: AdheipSingh Date: Mon, 23 Dec 2024 10:54:37 +0530 Subject: [PATCH 12/13] add kube context --- pkg/common/common.go | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/pkg/common/common.go b/pkg/common/common.go index 8f74354..f67f01c 100644 --- a/pkg/common/common.go +++ b/pkg/common/common.go @@ -116,7 +116,26 @@ func PromptK8sContext() (clusterName string, err error) { os.Exit(1) } - // Get current contexts + // Check if P_KUBE_CONTEXT is set + envContext := os.Getenv("P_KUBE_CONTEXT") + if envContext != "" { + // Validate if the context exists in kubeconfig + if _, exists := config.Contexts[envContext]; !exists { + return "", fmt.Errorf("context '%s' not found in kubeconfig", envContext) + } + + // Set current context to the value from P_KUBE_CONTEXT + config.CurrentContext = envContext + err = clientcmd.WriteToFile(*config, kubeconfigPath) + if err != nil { + return "", err + } + + fmt.Printf("\033[32mUsing Kubernetes context from P_KUBE_CONTEXT: %s āœ”\033[0m\n", envContext) + return envContext, nil + } + + // Get available contexts from kubeconfig currentContext := config.Contexts var contexts []string for i := range currentContext { From 9be2f438c499783676c3970b67d204f7a83026d8 Mon Sep 17 00:00:00 2001 From: AdheipSingh Date: Mon, 23 Dec 2024 11:41:21 +0530 Subject: [PATCH 13/13] update playground --- main.go | 17 ----------- pkg/installer/installer.go | 60 ++++++++++++++++++++++++++++++++++---- pkg/installer/plans.go | 10 +++++++ 3 files changed, 64 insertions(+), 23 deletions(-) diff --git a/main.go b/main.go index 3a44b3e..43d3245 100644 --- a/main.go +++ b/main.go @@ -186,23 +186,6 @@ var query = &cobra.Command{ }, } -var install = &cobra.Command{ - Use: "install", - Short: "Install parseable on kubernetes cluster", - Long: "\ninstall command is used to install 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, "install", args) - }() - }, -} - var cluster = &cobra.Command{ Use: "cluster", Short: "Cluster operations for parseable.", diff --git a/pkg/installer/installer.go b/pkg/installer/installer.go index 55f257c..c7c32df 100644 --- a/pkg/installer/installer.go +++ b/pkg/installer/installer.go @@ -68,6 +68,56 @@ func waterFall(verbose bool) { log.Fatalf("Failed to prompt for kubernetes context: %v", err) } + if plan.Name == "Playground" { + chartValues = append(chartValues, "parseable.store=local-store") + chartValues = append(chartValues, "parseable.localModeSecret.enabled=true") + + // Prompt for namespace and credentials + pbInfo, err := promptNamespaceAndCredentials() + if err != nil { + log.Fatalf("Failed to prompt for namespace and credentials: %v", err) + } + + // Prompt for agent deployment + _, agentValues, err := promptAgentDeployment(chartValues, *pbInfo) + if err != nil { + log.Fatalf("Failed to prompt for agent deployment: %v", err) + } + + if err := applyParseableSecret(pbInfo, LocalStore, ObjectStoreConfig{}); err != nil { + log.Fatalf("Failed to apply secret object store configuration: %v", err) + } + + // Define the deployment configuration + config := HelmDeploymentConfig{ + ReleaseName: pbInfo.Name, + Namespace: pbInfo.Namespace, + RepoName: "parseable", + RepoURL: "https://charts.parseable.com", + ChartName: "parseable", + Version: "1.6.6", + Values: agentValues, + Verbose: verbose, + } + + if err := deployRelease(config); err != nil { + log.Fatalf("Failed to deploy parseable, err: %v", err) + } + + if err := updateInstallerConfigMap(common.InstallerEntry{ + Name: pbInfo.Name, + Namespace: pbInfo.Namespace, + Version: config.Version, + Status: "success", + }); err != nil { + log.Fatalf("Failed to update parseable installer file, err: %v", err) + } + + printSuccessBanner(*pbInfo, config.Version, "parseable", "parseable") + + return + } + // pb supports only distributed deployments chartValues = append(chartValues, "parseable.highAvailability.enabled=true") @@ -124,7 +174,9 @@ func waterFall(verbose bool) { log.Fatalf("Failed to update parseable installer file, err: %v", err) } - printSuccessBanner(*pbInfo, config.Version) + ingestorURL, queryURL := getParseableSvcUrls(pbInfo.Name, pbInfo.Namespace) + + printSuccessBanner(*pbInfo, config.Version, ingestorURL, queryURL) } @@ -230,8 +282,6 @@ func applyParseableSecret(ps *ParseableInfo, store ObjectStore, objectStoreConfi secretManifest = getParseableSecretGcs(ps, objectStoreConfig) } - fmt.Println(secretManifest) - // apply the Kubernetes Secret if err := applyManifest(secretManifest); err != nil { return fmt.Errorf("failed to create and apply secret: %w", err) @@ -742,9 +792,7 @@ func deployRelease(config HelmDeploymentConfig) error { } // printSuccessBanner remains the same as in the original code -func printSuccessBanner(pbInfo ParseableInfo, version string) { - - ingestorURL, queryURL := getParseableSvcUrls(pbInfo.Name, pbInfo.Namespace) +func printSuccessBanner(pbInfo ParseableInfo, version, ingestorURL, queryURL string) { // Encode credentials to Base64 credentials := map[string]string{ diff --git a/pkg/installer/plans.go b/pkg/installer/plans.go index 0fcb6f0..cb86976 100644 --- a/pkg/installer/plans.go +++ b/pkg/installer/plans.go @@ -35,6 +35,15 @@ type Plan struct { // Plans define the plans with clear CPU and memory specs for consumption var Plans = map[string]Plan{ + "Playground": { + Name: "Playground", + IngestionSpeed: "100 events/sec", + PerDayIngestion: "~1GB", + QueryPerformance: "Basic performance", + CPUAndMemorySpecs: "1 CPUs, 1GB RAM", + CPU: "1", + Memory: "1Gi", + }, "Small": { Name: "Small", IngestionSpeed: "1000 events/sec", @@ -66,6 +75,7 @@ var Plans = map[string]Plan{ func promptUserPlanSelection() (Plan, error) { planList := []Plan{ + Plans["Playground"], Plans["Small"], Plans["Medium"], Plans["Large"],