diff --git a/cmd/cluster.go b/cmd/cluster.go
new file mode 100644
index 0000000..99bacfd
--- /dev/null
+++ b/cmd/cluster.go
@@ -0,0 +1,237 @@
+// 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",
+ 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")
+ installer.Installer(verbose)
+ },
+}
+
+// ListOssCmd lists the Parseable OSS servers
+var ListOssCmd = &cobra.Command{
+ Use: "list",
+ Short: "List available Parseable servers",
+ Example: "pb list",
+ 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 servers: %v", err)
+ }
+
+ // Check if there are no entries
+ if len(entries) == 0 {
+ fmt.Println("No clusters 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",
+ Short: "Show values available in Parseable 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: "uninstall",
+ Short: "Uninstall Parseable servers",
+ Example: "pb uninstall",
+ 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 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.")
+ return
+ }
+
+ // Prompt user to select a cluster
+ selectedCluster, err := common.PromptClusterSelection(entries)
+ if err != nil {
+ 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)) {
+ fmt.Println(common.Yellow + "Uninstall operation canceled.")
+ return
+ }
+
+ //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 {
+ 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)
+ },
+}
+
+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(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
+}
+
+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/cmd/installer.go b/cmd/installer.go
deleted file mode 100644
index a8c9116..0000000
--- a/cmd/installer.go
+++ /dev/null
@@ -1,270 +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 (
- "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"
-)
-
-var verbose bool
-
-var InstallOssCmd = &cobra.Command{
- Use: "oss",
- Short: "Deploy Parseable OSS",
- Example: "pb install oss",
- RunE: func(cmd *cobra.Command, _ []string) error {
- // Add verbose flag
- cmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Enable verbose logging")
-
- // Print the banner
- printBanner()
-
- // 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
- },
-}
-
-// 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
deleted file mode 100644
index 66b51e6..0000000
--- a/cmd/uninstaller.go
+++ /dev/null
@@ -1,44 +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 (
- "fmt"
-
- "pb/pkg/common"
- "pb/pkg/installer"
-
- "github.com/spf13/cobra"
-)
-
-var UnInstallOssCmd = &cobra.Command{
- Use: "oss",
- Short: "Uninstall Parseable OSS",
- Example: "pb uninstall oss",
- RunE: func(cmd *cobra.Command, _ []string) error {
- // Add verbose flag
- cmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Enable verbose logging")
-
- // Print the banner
- printBanner()
-
- if err := installer.Uninstaller(verbose); err != nil {
- fmt.Println(common.Red + err.Error())
- }
-
- return nil
- },
-}
diff --git a/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..43d3245 100644
--- a/main.go
+++ b/main.go
@@ -186,10 +186,44 @@ 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..",
+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",
+ 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 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" {
@@ -246,9 +280,16 @@ 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)
+
+ uninstall.AddCommand(pb.UninstallOssCmd)
- uninstall.AddCommand(pb.UnInstallOssCmd)
+ show.AddCommand(pb.ShowValuesCmd)
cli.AddCommand(profile)
cli.AddCommand(query)
@@ -256,11 +297,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)
// Set as command
pb.VersionCmd.Run = func(_ *cobra.Command, _ []string) {
diff --git a/pkg/common/common.go b/pkg/common/common.go
index 56271d2..f67f01c 100644
--- a/pkg/common/common.go
+++ b/pkg/common/common.go
@@ -15,6 +15,28 @@
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"
+)
+
+const (
+ configMapName = "parseable-installer"
+ namespace = "pb-system"
+ dataKey = "installer-data"
+)
+
// ANSI escape codes for colors
const (
Yellow = "\033[33m"
@@ -24,3 +46,235 @@ 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) {
+
+ // 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 + "────────────────────────────────────────────────────────────────────────────")
+ 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)
+ }
+
+ // 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 {
+ 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(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 = Yellow + infoMsg
+
+ 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.
diff --git a/pkg/installer/installer.go b/pkg/installer/installer.go
index dfc01d4..c7c32df 100644
--- a/pkg/installer/installer.go
+++ b/pkg/installer/installer.go
@@ -20,44 +20,115 @@ import (
"bytes"
"context"
"encoding/base64"
+ "encoding/json"
"fmt"
"log"
+ "net"
"os"
- "path/filepath"
+ "os/exec"
+ "runtime"
"strings"
+ "sync"
+ "time"
"pb/pkg/common"
+ "pb/pkg/helm"
"github.com/manifoldco/promptui"
- yamlv2 "gopkg.in/yaml.v2"
+ 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"
"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) {
- if _, err := promptK8sContext(); err != nil {
+func Installer(verbose bool) {
+ printBanner()
+ waterFall(verbose)
+}
+
+// waterFall orchestrates the installation process
+func waterFall(verbose bool) {
+ var chartValues []string
+ plan, err := promptUserPlanSelection()
+ if err != nil {
+ log.Fatalf("Failed to prompt for plan selection: %v", err)
+ }
+
+ _, err = common.PromptK8sContext()
+ if err != nil {
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")
// 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, *pbInfo)
if err != nil {
log.Fatalf("Failed to prompt for agent deployment: %v", err)
}
@@ -69,54 +140,105 @@ 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, plan)
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.6",
+ 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 := 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)
}
- return &valuesHolder, append(chartValues, storeConfigValues...)
+ ingestorURL, queryURL := getParseableSvcUrls(pbInfo.Name, pbInfo.Namespace)
+
+ printSuccessBanner(*pbInfo, config.Version, ingestorURL, queryURL)
+
}
-// 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 +261,8 @@ func promptNamespaceAndCredentials() (*ParseableSecret, error) {
}
password = strings.TrimSpace(password)
- return &ParseableSecret{
+ return &ParseableInfo{
+ Name: name,
Namespace: namespace,
Username: username,
Password: password,
@@ -147,7 +270,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 +291,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
@@ -178,7 +301,6 @@ metadata:
namespace: %s
type: Opaque
data:
-- addr
azr.access_key: %s
azr.account: %s
azr.container: %s
@@ -191,7 +313,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)),
@@ -203,7 +325,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 +361,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 +397,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 +425,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, 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"},
@@ -319,15 +441,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) {
- 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")
+ 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
@@ -359,7 +498,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
@@ -369,12 +515,18 @@ func promptStoreConfigs(store ObjectStore, chartValues []string) (ObjectStoreCon
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)
@@ -384,7 +536,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()
@@ -392,15 +550,32 @@ func promptStoreConfigs(store ObjectStore, chartValues []string) (ObjectStoreCon
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))
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()
@@ -408,18 +583,24 @@ func promptStoreConfigs(store ObjectStore, chartValues []string) (ObjectStoreCon
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))
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
}
@@ -519,81 +700,316 @@ 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
+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
}
-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)
+// 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 release name [%s] namespace [%s] ", config.ReleaseName, config.Namespace)
+ spinner := common.CreateDeploymentSpinner(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
}
- // Define config file path
- configPath := filepath.Join(configDir, valuesHolder.ParseableSecret.Namespace+".yaml")
+ // Check for errors
+ if err, ok := <-errCh; ok {
+ return err
+ }
- // Marshal values to YAML
- configBytes, err := yamlv2.Marshal(valuesHolder)
+ return nil
+}
+
+// printSuccessBanner remains the same as in the original code
+func printSuccessBanner(pbInfo ParseableInfo, version, ingestorURL, queryURL string) {
+
+ // Encode credentials to Base64
+ credentials := map[string]string{
+ "username": pbInfo.Username,
+ "password": pbInfo.Password,
+ }
+ credentialsJSON, err := json.Marshal(credentials)
if err != nil {
- return fmt.Errorf("failed to marshal config to YAML: %w", err)
+ fmt.Printf("failed to marshal credentials: %v\n", err)
+ return
}
- // Write config file
- if err := os.WriteFile(configPath, configBytes, 0o644); err != nil {
- return fmt.Errorf("failed to write config file: %w", err)
+ 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+pbInfo.Namespace)
+ fmt.Printf(" • Chart Version: %s\n", common.Blue+version)
+ 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")
+ 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, queryURL, localPort, pbInfo.Namespace)
+
+ if err = startPortForward(pbInfo.Namespace, queryURL, "80", localPort, false); err != nil {
+ fmt.Printf(common.Red+"failed to port-forward service: %s", err.Error())
}
- return nil
+ // 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)
}
-// promptK8sContext retrieves Kubernetes contexts from kubeconfig.
-func promptK8sContext() (clusterName string, err error) {
- kubeconfigPath := os.Getenv("KUBECONFIG")
- if kubeconfigPath == "" {
- kubeconfigPath = os.Getenv("HOME") + "/.kube/config"
+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
}
- // Load kubeconfig file
- config, err := clientcmd.LoadFromFile(kubeconfigPath)
+ // 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()
+}
+
+func updateInstallerConfigMap(entry common.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 {
- fmt.Printf("\033[31mError loading kubeconfig: %v\033[0m\n", err)
- os.Exit(1)
+ return fmt.Errorf("failed to load kubeconfig: %w", err)
}
- // Get current contexts
- currentContext := config.Contexts
- var contexts []string
- for i := range currentContext {
- contexts = append(contexts, i)
+ clientset, err := kubernetes.NewForConfig(config)
+ if err != nil {
+ return fmt.Errorf("failed to create Kubernetes client: %w", err)
}
- // 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 }}' ✔",
- },
+ // 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)
+ }
+ }
+
+ // 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",
}
- _, clusterName, err = promptK8s.Run()
+ // 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 {
- return "", err
+ 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
+ }
+
+ // Retrieve existing data and append the new entry
+ existingData := data["data"].(map[string]interface{})
+ 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)
+ }
}
+ entries = append(entries, entry)
- // Set current context as selected
- config.CurrentContext = clusterName
- err = clientcmd.WriteToFile(*config, kubeconfigPath)
+ // Marshal the updated data back to YAML
+ updatedData, err := yamling.Marshal(entries)
if err != nil {
- return "", err
+ return fmt.Errorf("failed to marshal updated data: %v", 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 clusterName, nil
+ 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/model.go b/pkg/installer/model.go
index 7710e9f..d7364d0 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
@@ -37,9 +27,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.
}
@@ -87,16 +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.
-}
-
-// 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.
+ 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 d37481e..cb86976 100644
--- a/pkg/installer/plans.go
+++ b/pkg/installer/plans.go
@@ -29,20 +29,29 @@ 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
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",
PerDayIngestion: "~10GB",
QueryPerformance: "Basic performance",
CPUAndMemorySpecs: "2 CPUs, 4GB RAM",
- CPU: 2,
- Memory: 4,
+ CPU: "2",
+ Memory: "4Gi",
},
"Medium": {
Name: "Medium",
@@ -50,8 +59,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,13 +68,14 @@ var Plans = map[string]Plan{
PerDayIngestion: "~1TB",
QueryPerformance: "High performance",
CPUAndMemorySpecs: "8 CPUs, 32GB RAM",
- CPU: 8,
- Memory: 32,
+ CPU: "8",
+ Memory: "32Gi",
},
}
-func PromptUserPlanSelection() (Plan, error) {
+func promptUserPlanSelection() (Plan, error) {
planList := []Plan{
+ Plans["Playground"],
Plans["Small"],
Plans["Medium"],
Plans["Large"],
@@ -80,14 +90,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,5 +110,14 @@ func PromptUserPlanSelection() (Plan, error) {
return Plan{}, fmt.Errorf("failed to select deployment type: %w", err)
}
- return planList[index], nil
+ 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, selectedPlan.IngestionSpeed, selectedPlan.PerDayIngestion,
+ selectedPlan.QueryPerformance, selectedPlan.CPUAndMemorySpecs)
+
+ return selectedPlan, nil
}
diff --git a/pkg/installer/spinner.go b/pkg/installer/spinner.go
deleted file mode 100644
index 6d51d01..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+" %s ", namespace)
-
- return s
-}
diff --git a/pkg/installer/uninstaller.go b/pkg/installer/uninstaller.go
index 5f0cb8c..ddb9d86 100644
--- a/pkg/installer/uninstaller.go
+++ b/pkg/installer/uninstaller.go
@@ -21,57 +21,85 @@ import (
"fmt"
"os"
"path/filepath"
- "strings"
- "time"
-
"pb/pkg/common"
"pb/pkg/helm"
+ "strings"
+ "time"
- "gopkg.in/yaml.v2"
+ "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 {
- // Load configuration from the parseable.yaml file
- configPath := filepath.Join(os.Getenv("HOME"), ".parseable", "parseable.yaml")
- config, err := loadParseableConfig(configPath)
+ // Define the installer file path
+ homeDir, err := os.UserHomeDir()
if err != nil {
- return fmt.Errorf("failed to load configuration: %v", err)
+ 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 []common.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]", entry.Name, entry.Namespace)
}
- if config == (&ValuesHolder{}) {
- return fmt.Errorf("no existing configuration found in ~/.parseable/parseable.yaml")
+ 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 }}",
+ },
}
- // Prompt for Kubernetes context
- _, err = promptK8sContext()
+ index, _, err := promptClusterSelect.Run()
if err != nil {
- return fmt.Errorf("failed to prompt for Kubernetes context: %v", err)
+ return fmt.Errorf("failed to prompt for cluster selection: %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))
+ selectedCluster := entries[index]
+
+ // 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 {
- return fmt.Errorf("Uninstall canceled.")
+ fmt.Println(common.Yellow + "Uninstall canceled." + common.Reset)
+ return nil
}
// Helm application configuration
helmApp := helm.Helm{
- ReleaseName: "parseable",
- Namespace: namespace,
+ ReleaseName: selectedCluster.Name,
+ Namespace: selectedCluster.Namespace,
RepoName: "parseable",
RepoURL: "https://charts.parseable.com",
ChartName: "parseable",
- Version: "1.6.5",
+ Version: selectedCluster.Version,
}
// Create a spinner
- spinner := createDeploymentSpinner(namespace, "Uninstalling parseable in ")
+ spinner := common.CreateDeploymentSpinner("Uninstalling Parseable in ")
// Redirect standard output if not in verbose mode
var oldStdout *os.File
@@ -96,15 +124,15 @@ func Uninstaller(verbose bool) error {
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)
+ // 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 namespace '%s': %v", namespace, cleanupErr)
+ 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, namespace)
+ fmt.Printf(common.Green+"Successfully uninstalled Parseable from namespace '%s'.\n"+common.Reset, selectedCluster.Namespace)
return nil
}
@@ -121,21 +149,8 @@ func promptUserConfirmation(message string) (bool, error) {
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 {
+// 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 {
@@ -148,26 +163,25 @@ func cleanupNamespaceWithClient(namespace string) error {
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)
+ // Create a context with a timeout for secret deletion
+ ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
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)
- }
+ // Define the secret name
+ secretName := "parseable-env-secret"
- // 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
+ // 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
}
- time.Sleep(2 * time.Second)
+ 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
}