diff --git a/_sample_configs/kubernetes_config.png b/_sample_configs/kubernetes_config.png new file mode 100644 index 00000000..de51ed7f Binary files /dev/null and b/_sample_configs/kubernetes_config.png differ diff --git a/_sample_configs/kubernetes_config.yml b/_sample_configs/kubernetes_config.yml new file mode 100644 index 00000000..b29352a9 --- /dev/null +++ b/_sample_configs/kubernetes_config.yml @@ -0,0 +1,21 @@ +wtf: + colors: + border: + focusable: darkslateblue + focused: orange + normal: gray + grid: + columns: [32, 32, 32, 32, 32, 32] + rows: [10, 10, 10, 10, 10, 10] + refreshInterval: 2 + mods: + kubernetes: + enabled: true + kubeconfig: /Users/testuser/.kube/config + namespaces: ["demo", "kube-system"] + objects: ["nodes","deployments", "pods"] + position: + top: 0 + left: 0 + height: 6 + width: 3 diff --git a/app/widget_maker.go b/app/widget_maker.go index cdde17cd..a6b1f471 100644 --- a/app/widget_maker.go +++ b/app/widget_maker.go @@ -27,6 +27,7 @@ import ( "github.com/wtfutil/wtf/modules/ipaddresses/ipinfo" "github.com/wtfutil/wtf/modules/jenkins" "github.com/wtfutil/wtf/modules/jira" + "github.com/wtfutil/wtf/modules/kubernetes" "github.com/wtfutil/wtf/modules/logger" "github.com/wtfutil/wtf/modules/mercurial" "github.com/wtfutil/wtf/modules/nbascore" @@ -144,6 +145,9 @@ func MakeWidget( case "jira": settings := jira.NewSettingsFromYAML(moduleName, moduleConfig, config) widget = jira.NewWidget(app, pages, settings) + case "kubernetes": + settings := kubernetes.NewSettingsFromYAML(moduleName, moduleConfig, config) + widget = kubernetes.NewWidget(app, settings) case "logger": settings := logger.NewSettingsFromYAML(moduleName, moduleConfig, config) widget = logger.NewWidget(app, settings) diff --git a/modules/kubernetes/client.go b/modules/kubernetes/client.go new file mode 100644 index 00000000..0b3b40af --- /dev/null +++ b/modules/kubernetes/client.go @@ -0,0 +1,37 @@ +package kubernetes + +import ( + "sync" + + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/clientcmd" +) + +var kubeClient *clientInstance +var clientOnce sync.Once + +type clientInstance struct { + Client kubernetes.Interface +} + +// getInstance returns a Kubernetes interface for a clientset +func (widget *Widget) getInstance() *clientInstance { + clientOnce.Do(func() { + if kubeClient == nil { + kubeClient = &clientInstance{ + Client: widget.getKubeClient(), + } + } + }) + return kubeClient +} + +// getKubeClient returns a kubernetes clientset for the kubeconfig provided +func (widget *Widget) getKubeClient() kubernetes.Interface { + config, _ := clientcmd.BuildConfigFromFlags("", widget.kubeconfig) + clientset, err := kubernetes.NewForConfig(config) + if err != nil { + panic(err.Error) + } + return clientset +} diff --git a/modules/kubernetes/settings.go b/modules/kubernetes/settings.go new file mode 100644 index 00000000..7cc27b48 --- /dev/null +++ b/modules/kubernetes/settings.go @@ -0,0 +1,32 @@ +package kubernetes + +import ( + "github.com/olebedev/config" + "github.com/wtfutil/wtf/cfg" + "github.com/wtfutil/wtf/utils" +) + +const defaultTitle = "Kubernetes" + +type Settings struct { + common *cfg.Common + + objects []string `help:"Kubernetes objects to show. Options are pods, deployments."` + title string `help:"Override the title of widget."` + kubeconfig string `help:"Location of a kubeconfig file."` + namespaces []string `help:"List of namespaces to watch. If blank, defaults to all namespaces."` +} + +func NewSettingsFromYAML(name string, moduleConfig *config.Config, globalConfig *config.Config) *Settings { + + settings := Settings{ + common: cfg.NewCommonSettingsFromModule(name, defaultTitle, moduleConfig, globalConfig), + + objects: utils.ToStrs(moduleConfig.UList("objects")), + title: moduleConfig.UString("title"), + kubeconfig: moduleConfig.UString("kubeconfig"), + namespaces: utils.ToStrs(moduleConfig.UList("namespaces")), + } + + return &settings +} diff --git a/modules/kubernetes/widget.go b/modules/kubernetes/widget.go new file mode 100644 index 00000000..a05e6990 --- /dev/null +++ b/modules/kubernetes/widget.go @@ -0,0 +1,209 @@ +package kubernetes + +import ( + "fmt" + + "github.com/rivo/tview" + "github.com/wtfutil/wtf/utils" + "github.com/wtfutil/wtf/view" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// Widget contains all the config for the widget +type Widget struct { + view.TextWidget + + objects []string + title string + kubeconfig string + namespaces []string + settings *Settings +} + +// NewWidget creates a new instance of the widget +func NewWidget(app *tview.Application, settings *Settings) *Widget { + widget := Widget{ + TextWidget: view.NewTextWidget(app, settings.common, false), + + objects: settings.objects, + title: settings.title, + kubeconfig: settings.kubeconfig, + namespaces: settings.namespaces, + settings: settings, + } + + widget.View.SetWrap(true) + + return &widget +} + +// Refresh executes the command and updates the view with the results +func (widget *Widget) Refresh() { + + title := widget.generateTitle() + + client := widget.getInstance() + + var content string + + // Debug Info + //content += fmt.Sprintf("Namespaces: %q %d\n", widget.namespaces, len(widget.namespaces)) + //content += fmt.Sprintf("Objects: %q %d\n\n", widget.objects, len(widget.objects)) + + if !utils.DoesNotInclude(widget.objects, "nodes") { + nodeList, nodeError := client.getNodes() + if nodeError != nil { + widget.Redraw(title, "[red] Error getting node data [white]\n", true) + return + } + content += "[red]Nodes[white]\n" + for _, node := range nodeList { + content += fmt.Sprintf("%s\n", node) + } + content += "\n" + } + + if !utils.DoesNotInclude(widget.objects, "deployments") { + deploymentList, deploymentError := client.getDeployments(widget.namespaces) + if deploymentError != nil { + widget.Redraw(title, "[red] Error getting deployment data [white]\n", true) + return + } + content += "[red]Deployments[white]\n" + for _, deployment := range deploymentList { + content += fmt.Sprintf("%s\n", deployment) + } + content += "\n" + } + + if !utils.DoesNotInclude(widget.objects, "pods") { + podList, podError := client.getPods(widget.namespaces) + if podError != nil { + widget.Redraw(title, "[red] Error getting pod data [white]\n", false) + return + } + content += "[red]Pods[white]\n" + for _, pod := range podList { + content += fmt.Sprintf("%s\n", pod) + } + content += "\n" + } + + widget.Redraw(title, content, false) +} + +/* -------------------- Unexported Functions -------------------- */ + +// generateTitle generates a title for the widget +func (widget *Widget) generateTitle() string { + if len(widget.title) != 0 { + return widget.title + } + title := "Kube" + + if len(widget.namespaces) == 1 { + title += fmt.Sprintf(" - Namespace: %s", widget.namespaces[0]) + } else if len(widget.namespaces) > 1 { + title += fmt.Sprintf(" - Namespaces: %q", widget.namespaces) + } + return title +} + +// getPods returns a slice of pod strings +func (client *clientInstance) getPods(namespaces []string) ([]string, error) { + var podList []string + if len(namespaces) != 0 { + for _, namespace := range namespaces { + pods, err := client.Client.CoreV1().Pods(namespace).List(metav1.ListOptions{}) + if err != nil { + return nil, err + } + + for _, pod := range pods.Items { + var podString string + status := pod.Status.Phase + name := pod.ObjectMeta.Name + if len(namespaces) == 1 { + podString = fmt.Sprintf("%-50s %s", name, status) + } else { + podString = fmt.Sprintf("%-20s %-50s %s", namespace, name, status) + } + podList = append(podList, podString) + } + } + } else { + pods, err := client.Client.CoreV1().Pods("").List(metav1.ListOptions{}) + if err != nil { + return nil, err + } + for _, pod := range pods.Items { + podString := fmt.Sprintf("%-20s %-50s %s", pod.ObjectMeta.Namespace, pod.ObjectMeta.Name, pod.Status.Phase) + podList = append(podList, podString) + } + } + + return podList, nil +} + +// get Deployments returns a string slice of pod strings +func (client *clientInstance) getDeployments(namespaces []string) ([]string, error) { + var deploymentList []string + if len(namespaces) != 0 { + for _, namespace := range namespaces { + deployments, err := client.Client.AppsV1().Deployments(namespace).List(metav1.ListOptions{}) + if err != nil { + return nil, err + } + + for _, deployment := range deployments.Items { + var deployString string + if len(namespaces) == 1 { + deployString = fmt.Sprintf("%-50s", deployment.ObjectMeta.Name) + } else { + deployString = fmt.Sprintf("%-20s %-50s", deployment.ObjectMeta.Namespace, deployment.ObjectMeta.Name) + } + deploymentList = append(deploymentList, deployString) + } + } + } else { + deployments, err := client.Client.AppsV1().Deployments("").List(metav1.ListOptions{}) + if err != nil { + return nil, err + } + + for _, deployment := range deployments.Items { + deployString := fmt.Sprintf("%-20s %-50s", deployment.ObjectMeta.Namespace, deployment.ObjectMeta.Name) + deploymentList = append(deploymentList, deployString) + } + } + return deploymentList, nil +} + +// getNodes returns a string slice of nodes +func (client *clientInstance) getNodes() ([]string, error) { + var nodeList []string + + nodes, err := client.Client.CoreV1().Nodes().List(metav1.ListOptions{}) + if err != nil { + return nil, err + } + + for _, node := range nodes.Items { + var nodeStatus string + for _, condition := range node.Status.Conditions { + if condition.Reason == "KubeletReady" { + if condition.Status == "True" { + nodeStatus = "Ready" + } else if condition.Reason == "False" { + nodeStatus = "NotReady" + } else { + nodeStatus = "Unknown" + } + } + } + nodeString := fmt.Sprintf("%-50s %s", node.ObjectMeta.Name, nodeStatus) + nodeList = append(nodeList, nodeString) + } + return nodeList, nil +}