From db5208c58b74575a315f6deddeff09107cd95af0 Mon Sep 17 00:00:00 2001 From: AdheipSingh Date: Wed, 18 Dec 2024 22:52:31 +0530 Subject: [PATCH] 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 +// }