diff --git a/cmd/install.go b/cmd/install.go index 44f88af..182c5dd 100644 --- a/cmd/install.go +++ b/cmd/install.go @@ -2,6 +2,9 @@ package cmd import ( "bufio" + "bytes" + "context" + "encoding/base64" "fmt" "log" "os" @@ -10,30 +13,74 @@ import ( "strings" "sync" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/manifoldco/promptui" "github.com/spf13/cobra" + "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/rest" + "k8s.io/client-go/restmapper" + "k8s.io/client-go/tools/clientcmd" ) +// InstallOssCmd deploys Parseable OSS var InstallOssCmd = &cobra.Command{ Use: "oss", Short: "Deploy Parseable OSS", Example: "pb install oss", RunE: func(cmd *cobra.Command, args []string) error { + printBanner() + // Prompt for Kubernetes context _, err := k8s.PromptK8sContext() if err != nil { return fmt.Errorf(red+"Error prompting Kubernetes context: %w"+reset, err) } - // Prompt user to enter a namespace with yellow color + // Prompt user for namespace fmt.Print(yellow + "Enter the Kubernetes namespace for deployment: " + reset) reader := bufio.NewReader(os.Stdin) - namespace, err := reader.ReadString('\n') + namespace, _ := reader.ReadString('\n') + namespace = strings.TrimSpace(namespace) + + // Prompt for username + fmt.Print(yellow + "Enter the Parseable username: " + reset) + username, _ := reader.ReadString('\n') + username = strings.TrimSpace(username) + + // Prompt for password + fmt.Print(yellow + "Enter the Parseable password: " + reset) + password, _ := reader.ReadString('\n') + password = strings.TrimSpace(password) + + // Encode username and password to base64 for the Secret + encodedUsername := base64.StdEncoding.EncodeToString([]byte(username)) + encodedPassword := base64.StdEncoding.EncodeToString([]byte(password)) + + // Prompt for deployment type + prompt := promptui.Select{ + Label: "Select Deployment Type", + Items: []string{"Standalone", "Distributed"}, + } + _, deploymentType, err := prompt.Run() if err != nil { - return fmt.Errorf(red+"Error reading namespace input: %w"+reset, err) + log.Fatalf("Prompt failed: %v", err) } - // Trim newline character and spaces from the input - namespace = strings.TrimSpace(namespace) + // Define Helm chart configuration based on deployment type + var chartValues []string + if deploymentType == "Standalone" { + chartValues = []string{ + "parseable.image.repository=nikhilsinhaparseable/parseable", + "parseable.image.tag=debug-issue"} + } else { + chartValues = []string{"nikhilsinhaparseable/parseable=debug-issue"} + } + // Helm application configuration apps := []helm.Helm{ { ReleaseName: "parseable", @@ -42,38 +89,54 @@ var InstallOssCmd = &cobra.Command{ RepoUrl: "https://charts.parseable.com", ChartName: "parseable", Version: "1.6.3", - Values: []string{"parseable.image.tag=v1.6.2"}, + Values: chartValues, }, } - // Create a WaitGroup to manage Go routines - var wg sync.WaitGroup + // Generate Kubernetes Secret manifest + secretManifest := fmt.Sprintf(` +apiVersion: v1 +kind: Secret +metadata: + name: parseable-env-secret + namespace: %s +type: Opaque +data: + username: %s + password: %s + addr: %s + fs.dir: %s + staging.dir: %s +`, namespace, encodedUsername, encodedPassword, "MC4wLjAuMDo4MDAw", "Li9kYXRh", "Li9zdGFnaW5n") - // Use a channel to capture errors from Go routines - errCh := make(chan error, len(apps)) + // Apply the Kubernetes Secret + if err := ApplyManifest(secretManifest); err != nil { + return fmt.Errorf(red+"Failed to create secret: %w"+reset, err) + } + // 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() // Mark this Go routine as done when it finishes + defer wg.Done() log.Printf("Deploying %s in namespace %s...", app.ReleaseName, app.Namespace) if err := helm.Apply(app); err != nil { log.Printf(red+"Failed to deploy %s: %v"+reset, app.ReleaseName, err) - errCh <- err // Send the error to the channel + errCh <- err return } - log.Printf(green+"%s deployed successfully."+reset, app.ReleaseName) - }(app) // Pass the app variable to the closure to avoid capturing issues + }(app) } - // Wait for all Go routines to complete wg.Wait() - close(errCh) // Close the error channel after all routines finish + close(errCh) - // Check for errors from Go routines + // Check for errors for err := range errCh { if err != nil { - return err // Return the first error encountered + return err } } @@ -81,3 +144,109 @@ var InstallOssCmd = &cobra.Command{ return nil }, } + +// ApplyManifest ensures the namespace exists and applies a Kubernetes manifest YAML to the cluster +func ApplyManifest(manifest string) error { + // Load kubeconfig and create a dynamic Kubernetes client + config, err := loadKubeConfig() + if err != nil { + return fmt.Errorf("failed to load kubeconfig: %w", err) + } + + dynamicClient, err := dynamic.NewForConfig(config) + if err != nil { + return fmt.Errorf("failed to create dynamic client: %w", err) + } + + // Parse the manifest YAML into an unstructured object + decoder := yaml.NewYAMLOrJSONDecoder(bytes.NewReader([]byte(manifest)), 1024) + var obj unstructured.Unstructured + if err := decoder.Decode(&obj); err != nil { + return fmt.Errorf("failed to decode manifest: %w", err) + } + + // Get the namespace from the manifest object + namespace := obj.GetNamespace() + + if namespace != "" { + // Ensure the namespace exists, create it if it doesn't + namespaceClient := dynamic.NewForConfigOrDie(config).Resource(schema.GroupVersionResource{ + Group: "", + Version: "v1", + Resource: "namespaces", + }) + + // Try to get the namespace + _, err := namespaceClient.Get(context.TODO(), namespace, metav1.GetOptions{}) + if err != nil { + // If namespace doesn't exist, create it + namespaceObj := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "Namespace", + "metadata": map[string]interface{}{ + "name": namespace, + }, + }, + } + _, err := namespaceClient.Create(context.TODO(), namespaceObj, metav1.CreateOptions{}) + if err != nil { + return fmt.Errorf("failed to create namespace %s: %w", namespace, err) + } + fmt.Printf("Namespace %s created successfully.\n", namespace) + } + } + + // Get the GroupVersionResource dynamically + gvr, err := getGVR(config, &obj) + if err != nil { + return fmt.Errorf("failed to get GVR: %w", err) + } + + // Apply the manifest using the dynamic client + _, err = dynamicClient.Resource(gvr).Namespace(namespace).Create(context.TODO(), &obj, metav1.CreateOptions{}) + if err != nil { + return fmt.Errorf("failed to apply manifest: %w", err) + } + + fmt.Println("Manifest applied successfully.") + return nil +} + +// loadKubeConfig loads the kubeconfig from the default location +func loadKubeConfig() (*rest.Config, error) { + kubeconfig := clientcmd.NewDefaultClientConfigLoadingRules().GetDefaultFilename() + return clientcmd.BuildConfigFromFlags("", kubeconfig) +} + +// getGVR fetches the GroupVersionResource for the provided object +func getGVR(config *rest.Config, obj *unstructured.Unstructured) (schema.GroupVersionResource, error) { + discoveryClient, err := discovery.NewDiscoveryClientForConfig(config) + if err != nil { + return schema.GroupVersionResource{}, fmt.Errorf("failed to create discovery client: %w", err) + } + + groupResources, err := restmapper.GetAPIGroupResources(discoveryClient) + if err != nil { + return schema.GroupVersionResource{}, fmt.Errorf("failed to get API group resources: %w", err) + } + + mapper := restmapper.NewDiscoveryRESTMapper(groupResources) + gvk := obj.GroupVersionKind() + mapping, err := mapper.RESTMapping(gvk.GroupKind(), gvk.Version) + if err != nil { + return schema.GroupVersionResource{}, fmt.Errorf("failed to get GVR mapping: %w", err) + } + + return mapping.Resource, nil +} + +// printBanner displays a welcome banner +func printBanner() { + banner := ` + -------------------------------------- + Welcome to Parseable OSS Installation + -------------------------------------- +` + fmt.Println(green + banner + reset) +}