From b4abcfd4f832f333d73836888195b624d2dc4b34 Mon Sep 17 00:00:00 2001 From: matst80 Date: Mon, 14 Jul 2025 16:59:51 +0200 Subject: [PATCH] code --- .gitignore | 0 README.md | 67 +++++++++ go.mod | 58 ++++++++ go.sum | 174 ++++++++++++++++++++++ main.go | 385 +++++++++++++++++++++++++++++++++++++++++++++++++ metrics-row.go | 150 +++++++++++++++++++ utils.go | 18 +++ 7 files changed, 852 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 metrics-row.go create mode 100644 utils.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/README.md b/README.md new file mode 100644 index 0000000..faa541d --- /dev/null +++ b/README.md @@ -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 diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..251b75d --- /dev/null +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..fed1589 --- /dev/null +++ b/go.sum @@ -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= diff --git a/main.go b/main.go new file mode 100644 index 0000000..422ca8b --- /dev/null +++ b/main.go @@ -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", "": + return + case "r": + updateUI() + ui.Render(grid) + + case "": + 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() +} diff --git a/metrics-row.go b/metrics-row.go new file mode 100644 index 0000000..fcf6774 --- /dev/null +++ b/metrics-row.go @@ -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 +} diff --git a/utils.go b/utils.go new file mode 100644 index 0000000..5099a13 --- /dev/null +++ b/utils.go @@ -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) + } +}