code
This commit is contained in:
0
.gitignore
vendored
Normal file
0
.gitignore
vendored
Normal file
67
README.md
Normal file
67
README.md
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
# go-k8s-btop
|
||||||
|
|
||||||
|
A terminal-based Kubernetes resource monitor, similar to htop/btop but for Kubernetes pods.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- List all pods in specified namespaces with their statuses
|
||||||
|
- Monitor CPU and memory usage with graphical bars
|
||||||
|
- Display logs from starting or restarting pods
|
||||||
|
- Color-coded status indicators (green for running, yellow for pending, red for failed)
|
||||||
|
- Live updates with configurable refresh interval
|
||||||
|
- Keyboard shortcuts for navigation and display options
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Clone the repository and build:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/matst80/go-k8s-btop.git
|
||||||
|
cd go-k8s-btop
|
||||||
|
go build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
./go-k8s-btop [options]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Command-line Options
|
||||||
|
|
||||||
|
- `-kubeconfig string`: Path to the kubeconfig file (default: "~/.kube/config")
|
||||||
|
- `-namespaces string`: Comma-separated list of namespaces to monitor (default: "dev,home,cart")
|
||||||
|
- `-refresh int`: Refresh interval in seconds (default: 5)
|
||||||
|
|
||||||
|
### Keyboard Shortcuts
|
||||||
|
|
||||||
|
- `q` or `Ctrl+C`: Quit the application
|
||||||
|
- `r`: Manually refresh the display
|
||||||
|
- `h`: Toggle help screen
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
Monitor default namespaces with default settings:
|
||||||
|
```bash
|
||||||
|
./go-k8s-btop
|
||||||
|
```
|
||||||
|
|
||||||
|
Monitor specific namespaces:
|
||||||
|
```bash
|
||||||
|
./go-k8s-btop -namespaces=kube-system,default
|
||||||
|
```
|
||||||
|
|
||||||
|
Use a custom refresh interval:
|
||||||
|
```bash
|
||||||
|
./go-k8s-btop -refresh=10
|
||||||
|
```
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Go 1.18 or higher
|
||||||
|
- A valid kubeconfig with access to the Kubernetes API
|
||||||
|
- Metrics API enabled in your Kubernetes cluster (for CPU/Memory usage)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
58
go.mod
Normal file
58
go.mod
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
module github.com/matst80/go-k8s-btop
|
||||||
|
|
||||||
|
go 1.24.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/dustin/go-humanize v1.0.1
|
||||||
|
github.com/fatih/color v1.18.0
|
||||||
|
github.com/gizak/termui/v3 v3.1.0
|
||||||
|
k8s.io/apimachinery v0.33.1
|
||||||
|
k8s.io/client-go v0.33.1
|
||||||
|
k8s.io/metrics v0.33.1
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
|
||||||
|
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
|
||||||
|
github.com/go-logr/logr v1.4.2 // indirect
|
||||||
|
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||||
|
github.com/go-openapi/jsonreference v0.20.2 // indirect
|
||||||
|
github.com/go-openapi/swag v0.23.0 // indirect
|
||||||
|
github.com/gogo/protobuf v1.3.2 // indirect
|
||||||
|
github.com/google/gnostic-models v0.6.9 // indirect
|
||||||
|
github.com/google/go-cmp v0.7.0 // indirect
|
||||||
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/josharian/intern v1.0.0 // indirect
|
||||||
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
|
github.com/mailru/easyjson v0.7.7 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/mattn/go-runewidth v0.0.2 // indirect
|
||||||
|
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
|
github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d // indirect
|
||||||
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
|
github.com/x448/float16 v0.8.4 // indirect
|
||||||
|
golang.org/x/net v0.38.0 // indirect
|
||||||
|
golang.org/x/oauth2 v0.27.0 // indirect
|
||||||
|
golang.org/x/sys v0.31.0 // indirect
|
||||||
|
golang.org/x/term v0.30.0 // indirect
|
||||||
|
golang.org/x/text v0.23.0 // indirect
|
||||||
|
golang.org/x/time v0.9.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.36.5 // indirect
|
||||||
|
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
|
||||||
|
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
k8s.io/api v0.33.1 // indirect
|
||||||
|
k8s.io/klog/v2 v2.130.1 // indirect
|
||||||
|
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect
|
||||||
|
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect
|
||||||
|
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
|
||||||
|
sigs.k8s.io/randfill v1.0.0 // indirect
|
||||||
|
sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect
|
||||||
|
sigs.k8s.io/yaml v1.4.0 // indirect
|
||||||
|
)
|
||||||
174
go.sum
Normal file
174
go.sum
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
|
github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
|
||||||
|
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
||||||
|
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||||
|
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||||
|
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
|
||||||
|
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
|
||||||
|
github.com/gizak/termui/v3 v3.1.0 h1:ZZmVDgwHl7gR7elfKf1xc4IudXZ5qqfDh4wExk4Iajc=
|
||||||
|
github.com/gizak/termui/v3 v3.1.0/go.mod h1:bXQEBkJpzxUAKf0+xq9MSWAvWZlE7c+aidmyFlkYTrY=
|
||||||
|
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||||
|
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
|
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
|
||||||
|
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
|
||||||
|
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
|
||||||
|
github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
|
||||||
|
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
|
||||||
|
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
|
||||||
|
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
|
||||||
|
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
|
||||||
|
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||||
|
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||||
|
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||||
|
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||||
|
github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw=
|
||||||
|
github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw=
|
||||||
|
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo=
|
||||||
|
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||||
|
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||||
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
|
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||||
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
|
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||||
|
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||||
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mattn/go-runewidth v0.0.2 h1:UnlwIPBGaTZfPQ6T1IGzPI0EkYAQmT9fAEJ/poFC63o=
|
||||||
|
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||||
|
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM=
|
||||||
|
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||||
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
|
github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d h1:x3S6kxmy49zXVVyhcnrFqxvNVCBPb2KZ9hV2RBdS840=
|
||||||
|
github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ=
|
||||||
|
github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM=
|
||||||
|
github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
|
||||||
|
github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4=
|
||||||
|
github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
|
||||||
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||||
|
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||||
|
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||||
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||||
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||||
|
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||||
|
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
|
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
|
||||||
|
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||||
|
golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M=
|
||||||
|
golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||||
|
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
|
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
|
||||||
|
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||||
|
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||||
|
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
|
||||||
|
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||||
|
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
|
golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
|
||||||
|
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
|
||||||
|
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
|
||||||
|
gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
|
||||||
|
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
||||||
|
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
k8s.io/api v0.33.1 h1:tA6Cf3bHnLIrUK4IqEgb2v++/GYUtqiu9sRVk3iBXyw=
|
||||||
|
k8s.io/api v0.33.1/go.mod h1:87esjTn9DRSRTD4fWMXamiXxJhpOIREjWOSjsW1kEHw=
|
||||||
|
k8s.io/apimachinery v0.33.1 h1:mzqXWV8tW9Rw4VeW9rEkqvnxj59k1ezDUl20tFK/oM4=
|
||||||
|
k8s.io/apimachinery v0.33.1/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=
|
||||||
|
k8s.io/client-go v0.33.1 h1:ZZV/Ks2g92cyxWkRRnfUDsnhNn28eFpt26aGc8KbXF4=
|
||||||
|
k8s.io/client-go v0.33.1/go.mod h1:JAsUrl1ArO7uRVFWfcj6kOomSlCv+JpvIsp6usAGefA=
|
||||||
|
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
|
||||||
|
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
|
||||||
|
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4=
|
||||||
|
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8=
|
||||||
|
k8s.io/metrics v0.33.1 h1:Ypd5ITCf+fM+LDNFk7hESXTc3vh02CQYGiwRoVRaGsM=
|
||||||
|
k8s.io/metrics v0.33.1/go.mod h1:wK8cFTK5ykBdhL0Wy4RZwLH28XM7j/Klc+NQrMRWVxg=
|
||||||
|
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro=
|
||||||
|
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||||
|
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8=
|
||||||
|
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo=
|
||||||
|
sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
|
||||||
|
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
|
||||||
|
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
|
||||||
|
sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc=
|
||||||
|
sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps=
|
||||||
|
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
|
||||||
|
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
|
||||||
385
main.go
Normal file
385
main.go
Normal file
@@ -0,0 +1,385 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/dustin/go-humanize"
|
||||||
|
ui "github.com/gizak/termui/v3"
|
||||||
|
"github.com/gizak/termui/v3/widgets"
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
"k8s.io/client-go/kubernetes"
|
||||||
|
"k8s.io/client-go/tools/clientcmd"
|
||||||
|
"k8s.io/client-go/util/homedir"
|
||||||
|
metricsv "k8s.io/metrics/pkg/client/clientset/versioned"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Helper function to generate a resource usage graph
|
||||||
|
func generateResourceGraph(value float64, maxValue float64, width int) string {
|
||||||
|
if maxValue <= 0 {
|
||||||
|
maxValue = 1.0 // Avoid division by zero
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate number of segments to fill
|
||||||
|
usedSegments := int(value / maxValue * float64(width))
|
||||||
|
if usedSegments < 0 {
|
||||||
|
usedSegments = 0
|
||||||
|
}
|
||||||
|
if usedSegments > width {
|
||||||
|
usedSegments = width
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the graph string
|
||||||
|
graph := ""
|
||||||
|
for i := 0; i < usedSegments; i++ {
|
||||||
|
graph += "▉"
|
||||||
|
}
|
||||||
|
for i := usedSegments; i < width; i++ {
|
||||||
|
graph += "░"
|
||||||
|
}
|
||||||
|
|
||||||
|
return graph
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to parse CPU and Memory usage for bar graphs
|
||||||
|
func parseResourceUsage(usage string) float64 {
|
||||||
|
if usage == "" {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse CPU usage (e.g., "125m" to 0.125)
|
||||||
|
if strings.HasSuffix(usage, "m") {
|
||||||
|
value, err := strconv.ParseFloat(strings.TrimSuffix(usage, "m"), 64)
|
||||||
|
if err == nil {
|
||||||
|
return value / 1000
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse memory usage (convert to MB for easier display)
|
||||||
|
if strings.HasSuffix(usage, "Mi") {
|
||||||
|
value, err := strconv.ParseFloat(strings.TrimSuffix(usage, "Mi"), 64)
|
||||||
|
if err == nil {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
} else if strings.HasSuffix(usage, "Gi") {
|
||||||
|
value, err := strconv.ParseFloat(strings.TrimSuffix(usage, "Gi"), 64)
|
||||||
|
if err == nil {
|
||||||
|
return value * 1024
|
||||||
|
}
|
||||||
|
} else if strings.HasSuffix(usage, "Ki") {
|
||||||
|
value, err := strconv.ParseFloat(strings.TrimSuffix(usage, "Ki"), 64)
|
||||||
|
if err == nil {
|
||||||
|
return value / 1024
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to parse as a plain number
|
||||||
|
value, err := strconv.ParseFloat(usage, 64)
|
||||||
|
if err == nil {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// getDefaultKubeconfig returns the default kubeconfig path
|
||||||
|
func getDefaultKubeconfig() string {
|
||||||
|
if home := homedir.HomeDir(); home != "" {
|
||||||
|
return filepath.Join(home, ".kube", "config")
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Command line arguments
|
||||||
|
kubeconfig := flag.String("kubeconfig", getDefaultKubeconfig(), "Path to the kubeconfig file")
|
||||||
|
namespaceFlag := flag.String("namespaces", "dev,home,cart", "Comma-separated list of namespaces to monitor")
|
||||||
|
refreshInterval := flag.Int("refresh", 5, "Refresh interval in seconds")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
// Parse namespaces into a slice
|
||||||
|
namespaceList := strings.Split(*namespaceFlag, ",")
|
||||||
|
|
||||||
|
// Show startup message with configurations
|
||||||
|
log.Printf("Starting k8s-btop with settings:")
|
||||||
|
log.Printf("- Kubeconfig: %s", *kubeconfig)
|
||||||
|
log.Printf("- Monitoring namespaces: %s", *namespaceFlag)
|
||||||
|
log.Printf("- Refresh interval: %d seconds", *refreshInterval)
|
||||||
|
|
||||||
|
config, err := clientcmd.BuildConfigFromFlags("", *kubeconfig)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error building kubeconfig: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
clientset, err := kubernetes.NewForConfig(config)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error creating Kubernetes clientset: %v", err)
|
||||||
|
}
|
||||||
|
metricsClient, err := metricsv.NewForConfig(config)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Warning: metrics API not available: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ui.Init(); err != nil {
|
||||||
|
log.Fatalf("Failed to initialize termui: %v", err)
|
||||||
|
}
|
||||||
|
defer ui.Close()
|
||||||
|
|
||||||
|
podTable := widgets.NewTable()
|
||||||
|
podTable.Title = "Pod List"
|
||||||
|
podTable.BorderStyle.Fg = ui.ColorGreen
|
||||||
|
podTable.TitleStyle.Fg = ui.ColorWhite
|
||||||
|
podTable.Rows = [][]string{
|
||||||
|
{"Namespace", "Name", "Status", "Ready", "Restarts", "CPU", "CPU Graph", "Memory", "Memory Graph", "Age", "Node"},
|
||||||
|
}
|
||||||
|
podTable.RowSeparator = false
|
||||||
|
podTable.ColumnWidths = []int{15, 20, 10, 10, 8, 8, 15, 8, 15, 10, 15}
|
||||||
|
|
||||||
|
// Create CPU usage bar chart with custom styling
|
||||||
|
cpuChart := widgets.NewBarChart()
|
||||||
|
cpuChart.Title = "CPU Usage (cores)"
|
||||||
|
cpuChart.TitleStyle.Fg = ui.ColorBlue
|
||||||
|
cpuChart.BorderStyle.Fg = ui.ColorBlue
|
||||||
|
cpuChart.BarWidth = 5
|
||||||
|
cpuChart.BarGap = 1
|
||||||
|
cpuChart.BarColors = []ui.Color{ui.ColorGreen, ui.ColorYellow, ui.ColorRed}
|
||||||
|
cpuChart.LabelStyles = []ui.Style{ui.NewStyle(ui.ColorBlue)}
|
||||||
|
cpuChart.NumFormatter = func(f float64) string {
|
||||||
|
return fmt.Sprintf("%.2f", f)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create memory usage bar chart with custom styling
|
||||||
|
memChart := widgets.NewBarChart()
|
||||||
|
memChart.Title = "Memory Usage (MB)"
|
||||||
|
memChart.TitleStyle.Fg = ui.ColorMagenta
|
||||||
|
memChart.BorderStyle.Fg = ui.ColorMagenta
|
||||||
|
memChart.BarWidth = 5
|
||||||
|
memChart.BarGap = 1
|
||||||
|
memChart.BarColors = []ui.Color{ui.ColorGreen, ui.ColorYellow, ui.ColorRed}
|
||||||
|
memChart.LabelStyles = []ui.Style{ui.NewStyle(ui.ColorMagenta)}
|
||||||
|
memChart.NumFormatter = func(f float64) string {
|
||||||
|
return fmt.Sprintf("%.0f", f)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a single grid for the UI
|
||||||
|
grid := ui.NewGrid()
|
||||||
|
termWidth, termHeight := ui.TerminalDimensions()
|
||||||
|
grid.SetRect(0, 0, termWidth, termHeight)
|
||||||
|
|
||||||
|
updateUI := func() {
|
||||||
|
// Fetch data from Kubernetes
|
||||||
|
pods, err := refreshData(clientset, metricsClient, namespaceList)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error refreshing data: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize table rows with header
|
||||||
|
rows := [][]string{
|
||||||
|
{"Namespace", "Name", "Status", "Ready", "Restarts", "CPU", "CPU Graph", "Memory", "Memory Graph", "Age", "Node"},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep track of starting pods for log display
|
||||||
|
var startingPods []PodInfo
|
||||||
|
|
||||||
|
type PodWithMetrics struct {
|
||||||
|
Pod PodInfo
|
||||||
|
CPUVal float64
|
||||||
|
MemVal float64
|
||||||
|
}
|
||||||
|
|
||||||
|
podMetrics := []PodWithMetrics{}
|
||||||
|
|
||||||
|
// Process pods if data was fetched successfully
|
||||||
|
if pods != nil {
|
||||||
|
for _, pod := range pods {
|
||||||
|
cpuVal := parseResourceUsage(pod.CPUUsage)
|
||||||
|
memVal := parseResourceUsage(pod.MemoryUsage)
|
||||||
|
|
||||||
|
// Check if pod is starting or restarting
|
||||||
|
if pod.IsStarting {
|
||||||
|
startingPods = append(startingPods, pod)
|
||||||
|
} // Create CPU usage graph
|
||||||
|
// Find the max value to scale
|
||||||
|
maxCPUVal := 1.0 // Default to 1 core
|
||||||
|
for _, p := range pods {
|
||||||
|
val := parseResourceUsage(p.CPUUsage)
|
||||||
|
if val > maxCPUVal {
|
||||||
|
maxCPUVal = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate the CPU usage graph
|
||||||
|
cpuGraph := generateResourceGraph(cpuVal, maxCPUVal, 15)
|
||||||
|
|
||||||
|
// Create memory usage graph
|
||||||
|
// Find max value to scale
|
||||||
|
maxMemVal := 1024.0 // Default to 1 GB
|
||||||
|
for _, p := range pods {
|
||||||
|
val := parseResourceUsage(p.MemoryUsage)
|
||||||
|
if val > maxMemVal {
|
||||||
|
maxMemVal = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate the memory usage graph
|
||||||
|
memGraph := generateResourceGraph(memVal, maxMemVal, 15)
|
||||||
|
|
||||||
|
rows = append(rows, []string{
|
||||||
|
pod.Namespace,
|
||||||
|
pod.Name,
|
||||||
|
pod.Status,
|
||||||
|
fmt.Sprintf("%d/%d", pod.ReadyContainers, pod.ContainerCount),
|
||||||
|
fmt.Sprintf("%d", pod.RestartCount),
|
||||||
|
pod.CPUUsage,
|
||||||
|
cpuGraph,
|
||||||
|
pod.MemoryUsage,
|
||||||
|
memGraph,
|
||||||
|
humanize.Time(time.Now().Add(-pod.Age)),
|
||||||
|
pod.Node,
|
||||||
|
})
|
||||||
|
|
||||||
|
// We already parsed the metrics above, no need to do it again
|
||||||
|
|
||||||
|
podMetrics = append(podMetrics, PodWithMetrics{
|
||||||
|
Pod: pod,
|
||||||
|
CPUVal: cpuVal,
|
||||||
|
MemVal: memVal,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the table and charts
|
||||||
|
podTable.Rows = rows
|
||||||
|
podTable.RowStyles = make(map[int]ui.Style)
|
||||||
|
|
||||||
|
// Set row styles based on pod status
|
||||||
|
for i, row := range rows {
|
||||||
|
if i == 0 {
|
||||||
|
// Header row - make it bold
|
||||||
|
podTable.RowStyles[i] = ui.NewStyle(ui.ColorWhite, ui.ColorClear, ui.ModifierBold)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
status := row[2]
|
||||||
|
switch status {
|
||||||
|
case "Running":
|
||||||
|
podTable.RowStyles[i] = ui.NewStyle(ui.ColorGreen)
|
||||||
|
case "Pending":
|
||||||
|
podTable.RowStyles[i] = ui.NewStyle(ui.ColorYellow)
|
||||||
|
case "Failed":
|
||||||
|
podTable.RowStyles[i] = ui.NewStyle(ui.ColorRed)
|
||||||
|
case "Succeeded":
|
||||||
|
podTable.RowStyles[i] = ui.NewStyle(ui.ColorBlue)
|
||||||
|
default:
|
||||||
|
podTable.RowStyles[i] = ui.NewStyle(ui.ColorWhite)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
starting := len(startingPods)
|
||||||
|
logs := make([]interface{}, 0)
|
||||||
|
logSize := 1.0
|
||||||
|
if starting > 0 {
|
||||||
|
logSize = 0.6
|
||||||
|
}
|
||||||
|
logs = append(logs, ui.NewCol(logSize, podTable))
|
||||||
|
for _, pod := range startingPods {
|
||||||
|
logsViewer := widgets.NewParagraph()
|
||||||
|
logsViewer.Title = pod.Name
|
||||||
|
logsViewer.BorderStyle.Fg = ui.ColorYellow
|
||||||
|
logsViewer.TitleStyle.Fg = ui.ColorYellow
|
||||||
|
logsViewer.Text = "No logs to display. Select a starting pod."
|
||||||
|
logsViewer.WrapText = true
|
||||||
|
podLogs := getPodLogs(clientset, startingPods[0], 20) // Show last 20 lines
|
||||||
|
logsViewer.Title = fmt.Sprintf("Logs: %s/%s", startingPods[0].Namespace, startingPods[0].Name)
|
||||||
|
logsViewer.Text = podLogs
|
||||||
|
logs = append(logs, ui.NewCol(0.4/float64(starting), logsViewer))
|
||||||
|
}
|
||||||
|
|
||||||
|
grid.Set(
|
||||||
|
ui.NewRow(1.0, logs...),
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initial UI update
|
||||||
|
updateUI()
|
||||||
|
ui.Render(grid)
|
||||||
|
|
||||||
|
// Set up ticker with configured refresh interval
|
||||||
|
ticker := time.NewTicker(time.Duration(*refreshInterval) * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
// Set up event handling
|
||||||
|
uiEvents := ui.PollEvents()
|
||||||
|
// Create help text widget but don't display it initially
|
||||||
|
helpText := widgets.NewParagraph()
|
||||||
|
helpText.Title = "Keyboard Shortcuts"
|
||||||
|
helpText.BorderStyle.Fg = ui.ColorCyan
|
||||||
|
helpText.TitleStyle.Fg = ui.ColorCyan
|
||||||
|
helpText.Text = `
|
||||||
|
q, Ctrl+C : Quit
|
||||||
|
r : Manual refresh
|
||||||
|
`
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case e := <-uiEvents:
|
||||||
|
switch e.ID {
|
||||||
|
case "q", "<C-c>":
|
||||||
|
return
|
||||||
|
case "r":
|
||||||
|
updateUI()
|
||||||
|
ui.Render(grid)
|
||||||
|
|
||||||
|
case "<Resize>":
|
||||||
|
payload := e.Payload.(ui.Resize)
|
||||||
|
grid.SetRect(0, 0, payload.Width, payload.Height)
|
||||||
|
ui.Clear()
|
||||||
|
ui.Render(grid)
|
||||||
|
}
|
||||||
|
case <-ticker.C:
|
||||||
|
updateUI()
|
||||||
|
ui.Render(grid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function for string slicing
|
||||||
|
func min(a, b int) int {
|
||||||
|
if a < b {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to get pod logs
|
||||||
|
func getPodLogs(clientset *kubernetes.Clientset, pod PodInfo, lines int64) string {
|
||||||
|
podLogOptions := corev1.PodLogOptions{
|
||||||
|
TailLines: &lines,
|
||||||
|
Follow: false, // We don't want to block
|
||||||
|
}
|
||||||
|
|
||||||
|
req := clientset.CoreV1().Pods(pod.Namespace).GetLogs(pod.Name, &podLogOptions)
|
||||||
|
podLogs, err := req.Stream(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Sprintf("Error getting logs: %v", err)
|
||||||
|
}
|
||||||
|
defer podLogs.Close()
|
||||||
|
|
||||||
|
buf := new(strings.Builder)
|
||||||
|
_, err = io.Copy(buf, podLogs)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Sprintf("Error copying logs: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
150
metrics-row.go
Normal file
150
metrics-row.go
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/dustin/go-humanize"
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
v1 "k8s.io/api/core/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/client-go/kubernetes"
|
||||||
|
metricsv "k8s.io/metrics/pkg/client/clientset/versioned"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PodInfo struct {
|
||||||
|
Name string
|
||||||
|
Namespace string
|
||||||
|
Status string
|
||||||
|
ReadyContainers int
|
||||||
|
ContainerCount int
|
||||||
|
RestartCount int
|
||||||
|
CPUUsage string
|
||||||
|
MemoryUsage string
|
||||||
|
Age time.Duration
|
||||||
|
Node string
|
||||||
|
IsStarting bool // Added to track if pod is starting or restarting
|
||||||
|
}
|
||||||
|
|
||||||
|
func refreshData(clientset *kubernetes.Clientset, metricsClient *metricsv.Clientset, namespaces []string) ([]PodInfo, error) {
|
||||||
|
pods, err := clientset.CoreV1().Pods("").List(context.Background(), metav1.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error listing pods: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a map for faster namespace lookups
|
||||||
|
namespaceMap := make(map[string]bool)
|
||||||
|
for _, ns := range namespaces {
|
||||||
|
namespaceMap[ns] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
podMetricsMap := make(map[string]map[string]struct {
|
||||||
|
CPU int64
|
||||||
|
Memory int64
|
||||||
|
})
|
||||||
|
if metricsClient != nil {
|
||||||
|
podMetrics, err := metricsClient.MetricsV1beta1().PodMetricses("").List(context.Background(), metav1.ListOptions{})
|
||||||
|
if err == nil {
|
||||||
|
for _, metric := range podMetrics.Items {
|
||||||
|
nsKey := metric.Namespace
|
||||||
|
// Only process metrics for specified namespaces
|
||||||
|
if !namespaceMap[nsKey] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := podMetricsMap[nsKey]; !ok {
|
||||||
|
podMetricsMap[nsKey] = make(map[string]struct {
|
||||||
|
CPU int64
|
||||||
|
Memory int64
|
||||||
|
})
|
||||||
|
}
|
||||||
|
var totalCPU, totalMemory int64
|
||||||
|
for _, container := range metric.Containers {
|
||||||
|
totalCPU += container.Usage.Cpu().MilliValue()
|
||||||
|
totalMemory += container.Usage.Memory().Value()
|
||||||
|
}
|
||||||
|
podMetricsMap[nsKey][metric.Name] = struct {
|
||||||
|
CPU int64
|
||||||
|
Memory int64
|
||||||
|
}{
|
||||||
|
CPU: totalCPU,
|
||||||
|
Memory: totalMemory,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var result []PodInfo
|
||||||
|
for _, pod := range pods.Items {
|
||||||
|
readyCount := 0
|
||||||
|
restarts := 0
|
||||||
|
nsKey := pod.Namespace
|
||||||
|
// Only process pods for specified namespaces
|
||||||
|
if !namespaceMap[nsKey] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, status := range pod.Status.ContainerStatuses {
|
||||||
|
if status.Ready {
|
||||||
|
readyCount++
|
||||||
|
}
|
||||||
|
restarts += int(status.RestartCount)
|
||||||
|
}
|
||||||
|
podInfo := PodInfo{
|
||||||
|
Name: pod.Name,
|
||||||
|
Namespace: pod.Namespace,
|
||||||
|
Status: string(pod.Status.Phase),
|
||||||
|
ContainerCount: len(pod.Spec.Containers),
|
||||||
|
ReadyContainers: readyCount,
|
||||||
|
RestartCount: restarts,
|
||||||
|
Age: time.Since(pod.CreationTimestamp.Time),
|
||||||
|
//IP: pod.Status.PodIP,
|
||||||
|
IsStarting: time.Since(pod.CreationTimestamp.Time) < 1*time.Minute || isPodStartingOrRestarting(pod),
|
||||||
|
Node: pod.Spec.NodeName,
|
||||||
|
}
|
||||||
|
if metrics, ok := podMetricsMap[pod.Namespace][pod.Name]; ok {
|
||||||
|
podInfo.CPUUsage = fmt.Sprintf("%dm", metrics.CPU)
|
||||||
|
podInfo.MemoryUsage = humanize.Bytes(uint64(metrics.Memory))
|
||||||
|
} else {
|
||||||
|
podInfo.CPUUsage = "N/A"
|
||||||
|
podInfo.MemoryUsage = "N/A"
|
||||||
|
}
|
||||||
|
result = append(result, podInfo)
|
||||||
|
}
|
||||||
|
sort.Slice(result, func(i, j int) bool {
|
||||||
|
if result[i].Namespace == result[j].Namespace {
|
||||||
|
return result[i].Name < result[j].Name
|
||||||
|
}
|
||||||
|
return result[i].Namespace < result[j].Namespace
|
||||||
|
})
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to check if pod is starting or restarting
|
||||||
|
func isPodStartingOrRestarting(pod v1.Pod) bool {
|
||||||
|
// Check if pod is in Pending state
|
||||||
|
if pod.Status.Phase == corev1.PodPending {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if any container is in waiting state with specific reasons
|
||||||
|
for _, containerStatus := range pod.Status.ContainerStatuses {
|
||||||
|
if containerStatus.State.Waiting != nil {
|
||||||
|
reason := containerStatus.State.Waiting.Reason
|
||||||
|
if reason == "ContainerCreating" ||
|
||||||
|
reason == "PodInitializing" ||
|
||||||
|
reason == "CrashLoopBackOff" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Check if container recently restarted (within last minute)
|
||||||
|
if containerStatus.LastTerminationState.Terminated != nil {
|
||||||
|
terminatedTime := containerStatus.LastTerminationState.Terminated.FinishedAt.Time
|
||||||
|
if time.Since(terminatedTime) < 60*time.Second {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
18
utils.go
Normal file
18
utils.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "github.com/fatih/color"
|
||||||
|
|
||||||
|
func getColor(status string) *color.Color {
|
||||||
|
switch status {
|
||||||
|
case "Running":
|
||||||
|
return color.New(color.FgGreen)
|
||||||
|
case "Pending":
|
||||||
|
return color.New(color.FgYellow)
|
||||||
|
case "Failed":
|
||||||
|
return color.New(color.FgRed)
|
||||||
|
case "Succeeded":
|
||||||
|
return color.New(color.FgBlue)
|
||||||
|
default:
|
||||||
|
return color.New(color.FgWhite)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user