Skip to content

Commit ab642cb

Browse files
AdheipSinghnitisht
andauthored
feat: uninstaller for Parseable installer (#75)
--------- Co-authored-by: Nitish Tiwari <[email protected]>
1 parent f86d7ef commit ab642cb

File tree

5 files changed

+278
-0
lines changed

5 files changed

+278
-0
lines changed

cmd/uninstaller.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
"pb/pkg/common"
6+
"pb/pkg/installer"
7+
8+
"github.com/spf13/cobra"
9+
)
10+
11+
var UnInstallOssCmd = &cobra.Command{
12+
Use: "oss",
13+
Short: "Uninstall Parseable OSS",
14+
Example: "pb uninstall oss",
15+
RunE: func(cmd *cobra.Command, _ []string) error {
16+
// Add verbose flag
17+
cmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Enable verbose logging")
18+
19+
// Print the banner
20+
printBanner()
21+
22+
if err := installer.Uninstaller(verbose); err != nil {
23+
fmt.Println(common.Red + err.Error())
24+
}
25+
26+
return nil
27+
},
28+
}

main.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,23 @@ var install = &cobra.Command{
203203
},
204204
}
205205

206+
var uninstall = &cobra.Command{
207+
Use: "uninstall",
208+
Short: "Uninstall parseable on kubernetes cluster",
209+
Long: "\nuninstall command is used to uninstall parseable oss/enterprise on k8s cluster..",
210+
PersistentPreRunE: combinedPreRun,
211+
PersistentPostRun: func(cmd *cobra.Command, args []string) {
212+
if os.Getenv("PB_ANALYTICS") == "disable" {
213+
return
214+
}
215+
wg.Add(1)
216+
go func() {
217+
defer wg.Done()
218+
analytics.PostRunAnalytics(cmd, "uninstall", args)
219+
}()
220+
},
221+
}
222+
206223
func main() {
207224
profile.AddCommand(pb.AddProfileCmd)
208225
profile.AddCommand(pb.RemoveProfileCmd)
@@ -231,6 +248,8 @@ func main() {
231248

232249
install.AddCommand(pb.InstallOssCmd)
233250

251+
uninstall.AddCommand(pb.UnInstallOssCmd)
252+
234253
cli.AddCommand(profile)
235254
cli.AddCommand(query)
236255
cli.AddCommand(stream)
@@ -240,6 +259,7 @@ func main() {
240259

241260
cli.AddCommand(pb.AutocompleteCmd)
242261
cli.AddCommand(install)
262+
cli.AddCommand(uninstall)
243263
cli.AddCommand(schema)
244264

245265
// Set as command

pkg/helm/helm.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,3 +350,48 @@ func Upgrade(h Helm) error {
350350
}
351351
return nil
352352
}
353+
354+
func Uninstall(h Helm, verbose bool) (*release.UninstallReleaseResponse, error) {
355+
356+
// Create a logger that does nothing by default
357+
silentLogger := func(_ string, _ ...interface{}) {}
358+
359+
// Create settings
360+
settings := cli.New()
361+
362+
// Create action configuration
363+
actionConfig := new(action.Configuration)
364+
365+
// Choose logging method based on verbose flag
366+
logMethod := silentLogger
367+
if verbose {
368+
logMethod = log.Printf
369+
}
370+
371+
// Initialize action configuration with chosen logger
372+
if err := actionConfig.Init(
373+
settings.RESTClientGetter(),
374+
h.Namespace,
375+
os.Getenv("HELM_DRIVER"),
376+
logMethod,
377+
); err != nil {
378+
return &release.UninstallReleaseResponse{}, fmt.Errorf("failed to initialize Helm configuration: %w", err)
379+
}
380+
381+
client := action.NewUninstall(actionConfig)
382+
// Setting Namespace
383+
settings.SetNamespace(h.Namespace)
384+
settings.EnvVars()
385+
386+
settings.EnvVars()
387+
388+
client.Wait = true
389+
client.Timeout = 5 * time.Minute
390+
391+
resp, err := client.Run(h.ReleaseName)
392+
if err != nil {
393+
return &release.UninstallReleaseResponse{}, err
394+
}
395+
396+
return resp, nil
397+
}

pkg/installer/spinner.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package installer
2+
3+
import (
4+
"fmt"
5+
"pb/pkg/common"
6+
"time"
7+
8+
"github.com/briandowns/spinner"
9+
)
10+
11+
func createDeploymentSpinner(namespace, infoMsg string) *spinner.Spinner {
12+
// Custom spinner with multiple character sets for dynamic effect
13+
spinnerChars := []string{
14+
"●", "○", "◉", "○", "◉", "○", "◉", "○", "◉",
15+
}
16+
17+
s := spinner.New(
18+
spinnerChars,
19+
120*time.Millisecond,
20+
spinner.WithColor(common.Yellow),
21+
spinner.WithSuffix(" ..."),
22+
)
23+
24+
s.Prefix = fmt.Sprintf(common.Yellow+infoMsg+" %s ", namespace)
25+
26+
return s
27+
}

pkg/installer/uninstaller.go

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
package installer
2+
3+
import (
4+
"bufio"
5+
"context"
6+
"fmt"
7+
"os"
8+
"path/filepath"
9+
"pb/pkg/common"
10+
"pb/pkg/helm"
11+
"strings"
12+
"time"
13+
14+
"gopkg.in/yaml.v2"
15+
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
16+
"k8s.io/client-go/kubernetes"
17+
)
18+
19+
func Uninstaller(verbose bool) error {
20+
// Load configuration from the parseable.yaml file
21+
configPath := filepath.Join(os.Getenv("HOME"), ".parseable", "parseable.yaml")
22+
config, err := loadParseableConfig(configPath)
23+
if err != nil {
24+
return fmt.Errorf("failed to load configuration: %v", err)
25+
}
26+
27+
if config == (&ValuesHolder{}) {
28+
return fmt.Errorf("no existing configuration found in ~/.parseable/parseable.yaml")
29+
}
30+
31+
// Prompt for Kubernetes context
32+
_, err = promptK8sContext()
33+
if err != nil {
34+
return fmt.Errorf("failed to prompt for Kubernetes context: %v", err)
35+
}
36+
37+
// Prompt user to confirm namespace
38+
namespace := config.ParseableSecret.Namespace
39+
confirm, err := promptUserConfirmation(fmt.Sprintf(common.Yellow+"Do you wish to uninstall Parseable from namespace '%s'?", namespace))
40+
if err != nil {
41+
return fmt.Errorf("failed to get user confirmation: %v", err)
42+
}
43+
if !confirm {
44+
return fmt.Errorf("Uninstall cancelled.")
45+
}
46+
47+
// Helm application configuration
48+
helmApp := helm.Helm{
49+
ReleaseName: "parseable",
50+
Namespace: namespace,
51+
RepoName: "parseable",
52+
RepoURL: "https://charts.parseable.com",
53+
ChartName: "parseable",
54+
Version: "1.6.5",
55+
}
56+
57+
// Create a spinner
58+
spinner := createDeploymentSpinner(namespace, "Uninstalling parseable in ")
59+
60+
// Redirect standard output if not in verbose mode
61+
var oldStdout *os.File
62+
if !verbose {
63+
oldStdout = os.Stdout
64+
_, w, _ := os.Pipe()
65+
os.Stdout = w
66+
}
67+
68+
spinner.Start()
69+
70+
// Run Helm uninstall
71+
_, err = helm.Uninstall(helmApp, verbose)
72+
spinner.Stop()
73+
74+
// Restore stdout
75+
if !verbose {
76+
os.Stdout = oldStdout
77+
}
78+
79+
if err != nil {
80+
return fmt.Errorf("failed to uninstall Parseable: %v", err)
81+
}
82+
83+
// Namespace cleanup using Kubernetes client
84+
fmt.Printf(common.Yellow+"Cleaning up namespace '%s'...\n"+common.Reset, namespace)
85+
cleanupErr := cleanupNamespaceWithClient(namespace)
86+
if cleanupErr != nil {
87+
return fmt.Errorf("failed to clean up namespace '%s': %v", namespace, cleanupErr)
88+
}
89+
90+
// Print success banner
91+
fmt.Printf(common.Green+"Successfully uninstalled Parseable from namespace '%s'.\n"+common.Reset, namespace)
92+
93+
return nil
94+
95+
}
96+
97+
// promptUserConfirmation prompts the user for a yes/no confirmation
98+
func promptUserConfirmation(message string) (bool, error) {
99+
reader := bufio.NewReader(os.Stdin)
100+
fmt.Printf("%s [y/N]: ", message)
101+
response, err := reader.ReadString('\n')
102+
if err != nil {
103+
return false, err
104+
}
105+
response = strings.TrimSpace(strings.ToLower(response))
106+
return response == "y" || response == "yes", nil
107+
}
108+
109+
// loadParseableConfig loads the configuration from the specified file
110+
func loadParseableConfig(path string) (*ValuesHolder, error) {
111+
data, err := os.ReadFile(path)
112+
if err != nil {
113+
return nil, err
114+
}
115+
var config ValuesHolder
116+
if err := yaml.Unmarshal(data, &config); err != nil {
117+
return nil, err
118+
}
119+
return &config, nil
120+
}
121+
122+
// cleanupNamespaceWithClient deletes the specified namespace using Kubernetes client-go
123+
func cleanupNamespaceWithClient(namespace string) error {
124+
// Load the kubeconfig
125+
config, err := loadKubeConfig()
126+
if err != nil {
127+
return fmt.Errorf("failed to load kubeconfig: %w", err)
128+
}
129+
130+
// Create the clientset
131+
clientset, err := kubernetes.NewForConfig(config)
132+
if err != nil {
133+
return fmt.Errorf("failed to create Kubernetes client: %v", err)
134+
}
135+
136+
// Create a context with a timeout for namespace deletion
137+
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
138+
defer cancel()
139+
140+
// Delete the namespace
141+
err = clientset.CoreV1().Namespaces().Delete(ctx, namespace, v1.DeleteOptions{})
142+
if err != nil {
143+
return fmt.Errorf("error deleting namespace: %v", err)
144+
}
145+
146+
// Wait for the namespace to be fully removed
147+
fmt.Printf("Waiting for namespace '%s' to be deleted...\n", namespace)
148+
for {
149+
_, err := clientset.CoreV1().Namespaces().Get(ctx, namespace, v1.GetOptions{})
150+
if err != nil {
151+
fmt.Printf("Namespace '%s' successfully deleted.\n", namespace)
152+
break
153+
}
154+
time.Sleep(2 * time.Second)
155+
}
156+
157+
return nil
158+
}

0 commit comments

Comments
 (0)