mirror of
https://github.com/taigrr/wtf
synced 2025-01-18 04:03:14 -08:00
Adding a Kubernetes module for displaying Kubernetes cluster resources and info
Signed-off-by: Andrew Suderman <andrew@sudermanjr.com>
This commit is contained in:
parent
97ca61ac97
commit
da0e1dbd5b
BIN
_sample_configs/kubernetes_config.png
Normal file
BIN
_sample_configs/kubernetes_config.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 278 KiB |
21
_sample_configs/kubernetes_config.yml
Normal file
21
_sample_configs/kubernetes_config.yml
Normal file
@ -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
|
@ -27,6 +27,7 @@ import (
|
|||||||
"github.com/wtfutil/wtf/modules/ipaddresses/ipinfo"
|
"github.com/wtfutil/wtf/modules/ipaddresses/ipinfo"
|
||||||
"github.com/wtfutil/wtf/modules/jenkins"
|
"github.com/wtfutil/wtf/modules/jenkins"
|
||||||
"github.com/wtfutil/wtf/modules/jira"
|
"github.com/wtfutil/wtf/modules/jira"
|
||||||
|
"github.com/wtfutil/wtf/modules/kubernetes"
|
||||||
"github.com/wtfutil/wtf/modules/logger"
|
"github.com/wtfutil/wtf/modules/logger"
|
||||||
"github.com/wtfutil/wtf/modules/mercurial"
|
"github.com/wtfutil/wtf/modules/mercurial"
|
||||||
"github.com/wtfutil/wtf/modules/nbascore"
|
"github.com/wtfutil/wtf/modules/nbascore"
|
||||||
@ -144,6 +145,9 @@ func MakeWidget(
|
|||||||
case "jira":
|
case "jira":
|
||||||
settings := jira.NewSettingsFromYAML(moduleName, moduleConfig, config)
|
settings := jira.NewSettingsFromYAML(moduleName, moduleConfig, config)
|
||||||
widget = jira.NewWidget(app, pages, settings)
|
widget = jira.NewWidget(app, pages, settings)
|
||||||
|
case "kubernetes":
|
||||||
|
settings := kubernetes.NewSettingsFromYAML(moduleName, moduleConfig, config)
|
||||||
|
widget = kubernetes.NewWidget(app, settings)
|
||||||
case "logger":
|
case "logger":
|
||||||
settings := logger.NewSettingsFromYAML(moduleName, moduleConfig, config)
|
settings := logger.NewSettingsFromYAML(moduleName, moduleConfig, config)
|
||||||
widget = logger.NewWidget(app, settings)
|
widget = logger.NewWidget(app, settings)
|
||||||
|
37
modules/kubernetes/client.go
Normal file
37
modules/kubernetes/client.go
Normal file
@ -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
|
||||||
|
}
|
32
modules/kubernetes/settings.go
Normal file
32
modules/kubernetes/settings.go
Normal file
@ -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
|
||||||
|
}
|
209
modules/kubernetes/widget.go
Normal file
209
modules/kubernetes/widget.go
Normal file
@ -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
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user