Skip to content

Commit

Permalink
add list
Browse files Browse the repository at this point in the history
  • Loading branch information
AdheipSingh committed Dec 21, 2024
1 parent 2a86d6c commit c835c1d
Show file tree
Hide file tree
Showing 4 changed files with 146 additions and 73 deletions.
84 changes: 48 additions & 36 deletions cmd/list.go
Original file line number Diff line number Diff line change
@@ -1,31 +1,21 @@
package cmd

// Copyright (c) 2024 Parseable, Inc
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

import (
"context"
"fmt"
"log"
"os"
"path/filepath"

"pb/pkg/common"
"pb/pkg/installer"

"github.com/olekukonko/tablewriter"
"github.com/spf13/cobra"
"gopkg.in/yaml.v2"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
)

// ListOssCmd lists the Parseable OSS servers
Expand All @@ -34,8 +24,13 @@ var ListOssCmd = &cobra.Command{
Short: "List available Parseable OSS servers",
Example: "pb list oss",
Run: func(cmd *cobra.Command, _ []string) {
// Read the installer file
entries, err := readInstallerFile()
_, err := installer.PromptK8sContext()
if err != nil {
log.Fatalf("Failed to prompt for kubernetes context: %v", err)
}

// Read the installer data from the ConfigMap
entries, err := readInstallerConfigMap()
if err != nil {
log.Fatalf("Failed to list OSS servers: %v", err)
}
Expand All @@ -48,44 +43,61 @@ var ListOssCmd = &cobra.Command{

// Display the entries in a table format
table := tablewriter.NewWriter(os.Stdout)
table.SetHeader([]string{"Name", "Namespace", "Version", "Kubernetes Context", "Status"})
table.SetHeader([]string{"Name", "Namespace", "Version", "Status"})

for _, entry := range entries {
table.Append([]string{entry.Name, entry.Namespace, entry.Version, entry.Context, entry.Status})
table.Append([]string{entry.Name, entry.Namespace, entry.Version, entry.Status})
}

table.Render()
},
}

// readInstallerFile reads and parses the installer.yaml file
func readInstallerFile() ([]installer.InstallerEntry, error) {
// Define the file path
homeDir, err := os.UserHomeDir()
// readInstallerConfigMap fetches and parses installer data from a ConfigMap
func readInstallerConfigMap() ([]installer.InstallerEntry, error) {
const (
configMapName = "parseable-installer"
namespace = "pb-system"
dataKey = "installer-data"
)

// Load kubeconfig and create a Kubernetes client
config, err := loadKubeConfig()
if err != nil {
return nil, fmt.Errorf("failed to load kubeconfig: %w", err)
}

clientset, err := kubernetes.NewForConfig(config)
if err != nil {
return nil, fmt.Errorf("failed to get user home directory: %w", err)
return nil, fmt.Errorf("failed to create Kubernetes client: %w", err)
}
filePath := filepath.Join(homeDir, ".parseable", "pb", "installer.yaml")

// Check if the file exists
if _, err := os.Stat(filePath); os.IsNotExist(err) {
// Get the ConfigMap
cm, err := clientset.CoreV1().ConfigMaps(namespace).Get(context.TODO(), configMapName, metav1.GetOptions{})
if err != nil {
return nil, fmt.Errorf("failed to fetch ConfigMap: %w", err)
}

// Retrieve and parse the installer data
rawData, ok := cm.Data[dataKey]
if !ok {
fmt.Println(common.Yellow + "\n────────────────────────────────────────────────────────────────────────────")
fmt.Println(common.Yellow + "⚠️ No Parseable clusters found!")
fmt.Println(common.Yellow + "To get started, run: `pb install oss`")
fmt.Println(common.Yellow + "────────────────────────────────────────────────────────────────────────────\n")

Check failure on line 87 in cmd/list.go

View workflow job for this annotation

GitHub Actions / Build and Test the Go code

fmt.Println arg list ends with redundant newline
return nil, nil
}

// Read and parse the file
data, err := os.ReadFile(filePath)
if err != nil {
return nil, fmt.Errorf("failed to read installer file: %w", err)
}

var entries []installer.InstallerEntry
if err := yaml.Unmarshal(data, &entries); err != nil {
return nil, fmt.Errorf("failed to parse installer file: %w", err)
if err := yaml.Unmarshal([]byte(rawData), &entries); err != nil {
return nil, fmt.Errorf("failed to parse ConfigMap data: %w", err)
}

return entries, nil
}

// loadKubeConfig loads the kubeconfig from the default location
func loadKubeConfig() (*rest.Config, error) {
kubeconfig := clientcmd.NewDefaultClientConfigLoadingRules().GetDefaultFilename()
return clientcmd.BuildConfigFromFlags("", kubeconfig)
}
132 changes: 97 additions & 35 deletions pkg/installer/installer.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import (
"net"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"sync"
Expand All @@ -36,7 +35,9 @@ import (
"pb/pkg/helm"

"github.com/manifoldco/promptui"
yamlv3 "gopkg.in/yaml.v3"
yamling "gopkg.in/yaml.v3"
v1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
Expand All @@ -62,7 +63,7 @@ func waterFall(verbose bool) {
log.Fatalf("Failed to prompt for plan selection: %v", err)
}

context, err := promptK8sContext()
_, err = PromptK8sContext()
if err != nil {
log.Fatalf("Failed to prompt for kubernetes context: %v", err)
}
Expand Down Expand Up @@ -110,19 +111,17 @@ func waterFall(verbose bool) {
Verbose: verbose,
}

if err := deployRelease(config); err != nil {
log.Fatalf("Failed to deploy parseable, err: %v", err)
}

if err := updateInstallerFile(InstallerEntry{
if err := updateInstallerConfigMap(InstallerEntry{
Name: pbInfo.Name,
Namespace: pbInfo.Namespace,
Version: config.Version,
Context: context,
Status: "success",
}); err != nil {
log.Fatalf("Failed to update parseable installer file, err: %v", err)
}
if err := deployRelease(config); err != nil {
log.Fatalf("Failed to deploy parseable, err: %v", err)
}
printSuccessBanner(*pbInfo, config.Version)

}
Expand Down Expand Up @@ -637,8 +636,8 @@ func promptForInput(label string) string {
return strings.TrimSpace(input)
}

// promptK8sContext retrieves Kubernetes contexts from kubeconfig.
func promptK8sContext() (clusterName string, err error) {
// PromptK8sContext retrieves Kubernetes contexts from kubeconfig.
func PromptK8sContext() (clusterName string, err error) {
kubeconfigPath := os.Getenv("KUBECONFIG")
if kubeconfigPath == "" {
kubeconfigPath = os.Getenv("HOME") + "/.kube/config"
Expand Down Expand Up @@ -868,45 +867,108 @@ func openBrowser(url string) {
cmd.Start()
}

// updateInstallerFile updates or creates the installer.yaml file with deployment info
func updateInstallerFile(entry InstallerEntry) error {
// Define the file path
homeDir, err := os.UserHomeDir()
func updateInstallerConfigMap(entry InstallerEntry) error {
const (
configMapName = "parseable-installer"
namespace = "pb-system"
dataKey = "installer-data"
)

// Load kubeconfig and create a Kubernetes client
config, err := loadKubeConfig()
if err != nil {
return fmt.Errorf("failed to get user home directory: %w", err)
return fmt.Errorf("failed to load kubeconfig: %w", err)
}
filePath := filepath.Join(homeDir, ".parseable", "pb", "installer.yaml")

// Create the directory if it doesn't exist
if err := os.MkdirAll(filepath.Dir(filePath), 0755); err != nil {
return fmt.Errorf("failed to create directory for installer file: %w", err)
clientset, err := kubernetes.NewForConfig(config)
if err != nil {
return fmt.Errorf("failed to create Kubernetes client: %w", err)
}

// Read existing entries if the file exists
var entries []InstallerEntry
if _, err := os.Stat(filePath); err == nil {
// File exists, load existing content
data, err := os.ReadFile(filePath)
if err != nil {
return fmt.Errorf("failed to read existing installer file: %w", err)
// Ensure the namespace exists
_, err = clientset.CoreV1().Namespaces().Get(context.TODO(), namespace, metav1.GetOptions{})
if err != nil {
if apierrors.IsNotFound(err) {
_, err = clientset.CoreV1().Namespaces().Create(context.TODO(), &v1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: namespace,
},
}, metav1.CreateOptions{})
if err != nil {
return fmt.Errorf("failed to create namespace: %v", err)
}
} else {
return fmt.Errorf("failed to check namespace existence: %v", err)
}
}

if err := yaml.Unmarshal(data, &entries); err != nil {
return fmt.Errorf("failed to parse existing installer file: %w", err)
// Create a dynamic Kubernetes client
dynamicClient, err := dynamic.NewForConfig(config)
if err != nil {
return fmt.Errorf("failed to create dynamic client: %w", err)
}

// Define the ConfigMap resource
configMapResource := schema.GroupVersionResource{
Group: "", // Core resources have an empty group
Version: "v1",
Resource: "configmaps",
}

// Fetch the existing ConfigMap or initialize a new one
cm, err := dynamicClient.Resource(configMapResource).Namespace(namespace).Get(context.TODO(), configMapName, metav1.GetOptions{})
var data map[string]interface{}
if err != nil {
if !apierrors.IsNotFound(err) {
return fmt.Errorf("failed to fetch ConfigMap: %v", err)
}
// If not found, initialize a new ConfigMap
data = map[string]interface{}{
"metadata": map[string]interface{}{
"name": configMapName,
"namespace": namespace,
},
"data": map[string]interface{}{},
}
} else {
data = cm.Object
}

// Append the new entry
// Retrieve existing data and append the new entry
existingData := data["data"].(map[string]interface{})
var entries []InstallerEntry
if raw, ok := existingData[dataKey]; ok {
if err := yaml.Unmarshal([]byte(raw.(string)), &entries); err != nil {
return fmt.Errorf("failed to parse existing ConfigMap data: %v", err)
}
}
entries = append(entries, entry)

// Write the updated entries back to the file
data, err := yamlv3.Marshal(entries)
// Marshal the updated data back to YAML
updatedData, err := yamling.Marshal(entries)
if err != nil {
return fmt.Errorf("failed to marshal installer data: %w", err)
return fmt.Errorf("failed to marshal updated data: %v", err)
}

if err := os.WriteFile(filePath, data, 0644); err != nil {
return fmt.Errorf("failed to write installer file: %w", err)
// Update the ConfigMap data
existingData[dataKey] = string(updatedData)
data["data"] = existingData

// Apply the ConfigMap
if cm == nil {
_, err = dynamicClient.Resource(configMapResource).Namespace(namespace).Create(context.TODO(), &unstructured.Unstructured{
Object: data,
}, metav1.CreateOptions{})
if err != nil {
return fmt.Errorf("failed to create ConfigMap: %v", err)
}
} else {
_, err = dynamicClient.Resource(configMapResource).Namespace(namespace).Update(context.TODO(), &unstructured.Unstructured{
Object: data,
}, metav1.UpdateOptions{})
if err != nil {
return fmt.Errorf("failed to update ConfigMap: %v", err)
}
}

return nil
Expand Down
1 change: 0 additions & 1 deletion pkg/installer/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,5 @@ type InstallerEntry struct {
Name string `yaml:"name"`
Namespace string `yaml:"namespace"`
Version string `yaml:"version"`
Context string `yaml:"context"`
Status string `yaml:"status"` // todo ideally should be a heartbeat
}
2 changes: 1 addition & 1 deletion pkg/installer/uninstaller.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ func Uninstaller(verbose bool) error {
// Prompt the user to select a cluster
clusterNames := make([]string, len(entries))
for i, entry := range entries {
clusterNames[i] = fmt.Sprintf("[Name: %s] [Namespace: %s] [Context: %s]", entry.Name, entry.Namespace, entry.Context)
clusterNames[i] = fmt.Sprintf("[Name: %s] [Namespace: %s] [Context: %s]", entry.Name, entry.Namespace)
}

Check failure on line 61 in pkg/installer/uninstaller.go

View workflow job for this annotation

GitHub Actions / Build and Test the Go code

fmt.Sprintf format %s reads arg #3, but call has 2 args

promptClusterSelect := promptui.Select{
Expand Down

0 comments on commit c835c1d

Please sign in to comment.