016f93ab4f
Add a monorepo for small single-purpose CLI tools that share a common library, so tools reuse TUI primitives and Kubernetes client bootstrapping instead of duplicating them. - common/tui: direction markers, style palette, width-aware padding/layout - common/kube: in-cluster→kubeconfig client loading + core/metrics clients - podgap: live TUI comparing pod requests/limits vs actual usage, with up/down markers showing per-pod change since the last refresh - Makefile auto-discovers tool directories: make <tool> | all | install
187 lines
5.1 KiB
Go
187 lines
5.1 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"sort"
|
|
|
|
corev1 "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"
|
|
)
|
|
|
|
// row is one pod's aggregated (summed across containers) request/limit/usage.
|
|
type row struct {
|
|
Namespace string `json:"namespace"`
|
|
Pod string `json:"pod"`
|
|
|
|
CPUReqM int64 `json:"cpuRequestMillicores"`
|
|
CPULimM int64 `json:"cpuLimitMillicores"`
|
|
CPUUseM int64 `json:"cpuUsageMillicores"`
|
|
MemReqB int64 `json:"memRequestBytes"`
|
|
MemLimB int64 `json:"memLimitBytes"`
|
|
MemUseB int64 `json:"memUsageBytes"`
|
|
}
|
|
|
|
func (r row) cpuUtil() float64 {
|
|
if r.CPUReqM == 0 {
|
|
return 0
|
|
}
|
|
return float64(r.CPUUseM) / float64(r.CPUReqM)
|
|
}
|
|
|
|
func (r row) memUtil() float64 {
|
|
if r.MemReqB == 0 {
|
|
return 0
|
|
}
|
|
return float64(r.MemUseB) / float64(r.MemReqB)
|
|
}
|
|
|
|
// cpuWasteM / memWasteB are reserved-but-unused amounts (negative = over request).
|
|
func (r row) cpuWasteM() int64 { return r.CPUReqM - r.CPUUseM }
|
|
func (r row) memWasteB() int64 { return r.MemReqB - r.MemUseB }
|
|
|
|
// collect lists pod specs and pod metrics and joins them by namespace/name.
|
|
func collect(ctx context.Context, core kubernetes.Interface, mc metricsv.Interface, ns string) ([]row, error) {
|
|
pods, err := core.CoreV1().Pods(ns).List(ctx, metav1.ListOptions{})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list pods: %w", err)
|
|
}
|
|
metrics, err := mc.MetricsV1beta1().PodMetricses(ns).List(ctx, metav1.ListOptions{})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list pod metrics: %w", err)
|
|
}
|
|
|
|
type usage struct{ cpuM, memB int64 }
|
|
used := make(map[string]usage, len(metrics.Items))
|
|
for _, pm := range metrics.Items {
|
|
var u usage
|
|
for _, c := range pm.Containers {
|
|
u.cpuM += c.Usage.Cpu().MilliValue()
|
|
u.memB += c.Usage.Memory().Value()
|
|
}
|
|
used[pm.Namespace+"/"+pm.Name] = u
|
|
}
|
|
|
|
rows := make([]row, 0, len(pods.Items))
|
|
for _, p := range pods.Items {
|
|
// Only running pods have meaningful live usage to compare against.
|
|
if p.Status.Phase != corev1.PodRunning {
|
|
continue
|
|
}
|
|
r := row{Namespace: p.Namespace, Pod: p.Name}
|
|
for _, c := range p.Spec.Containers {
|
|
r.CPUReqM += c.Resources.Requests.Cpu().MilliValue()
|
|
r.CPULimM += c.Resources.Limits.Cpu().MilliValue()
|
|
r.MemReqB += c.Resources.Requests.Memory().Value()
|
|
r.MemLimB += c.Resources.Limits.Memory().Value()
|
|
}
|
|
if u, ok := used[p.Namespace+"/"+p.Name]; ok {
|
|
r.CPUUseM = u.cpuM
|
|
r.MemUseB = u.memB
|
|
}
|
|
rows = append(rows, r)
|
|
}
|
|
return rows, nil
|
|
}
|
|
|
|
func filterRows(rows []row, opts options) []row {
|
|
if !opts.overOnly && !opts.underOnly {
|
|
return rows
|
|
}
|
|
out := rows[:0]
|
|
for _, r := range rows {
|
|
if opts.overOnly {
|
|
overCPU := r.CPUReqM > 0 && r.cpuUtil() < opts.threshold
|
|
overMem := r.MemReqB > 0 && r.memUtil() < opts.threshold
|
|
if overCPU || overMem {
|
|
out = append(out, r)
|
|
continue
|
|
}
|
|
}
|
|
if opts.underOnly {
|
|
underCPU := r.CPUReqM > 0 && r.CPUUseM > r.CPUReqM
|
|
underMem := r.MemReqB > 0 && r.MemUseB > r.MemReqB
|
|
overLimCPU := r.CPULimM > 0 && r.CPUUseM > r.CPULimM*9/10
|
|
overLimMem := r.MemLimB > 0 && r.MemUseB > r.MemLimB*9/10
|
|
if underCPU || underMem || overLimCPU || overLimMem {
|
|
out = append(out, r)
|
|
}
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func sortRows(rows []row, key string) {
|
|
less := map[string]func(a, b row) bool{
|
|
"cpu-waste": func(a, b row) bool { return a.cpuWasteM() > b.cpuWasteM() },
|
|
"mem-waste": func(a, b row) bool { return a.memWasteB() > b.memWasteB() },
|
|
"cpu-util": func(a, b row) bool { return a.cpuUtil() > b.cpuUtil() },
|
|
"mem-util": func(a, b row) bool { return a.memUtil() > b.memUtil() },
|
|
"cpu-use": func(a, b row) bool { return a.CPUUseM > b.CPUUseM },
|
|
"mem-use": func(a, b row) bool { return a.MemUseB > b.MemUseB },
|
|
"name": func(a, b row) bool { return a.Namespace+a.Pod < b.Namespace+b.Pod },
|
|
}
|
|
fn, ok := less[key]
|
|
if !ok {
|
|
fn = less["cpu-waste"]
|
|
}
|
|
sort.SliceStable(rows, func(i, j int) bool { return fn(rows[i], rows[j]) })
|
|
}
|
|
|
|
func render(w io.Writer, rows []row, format string) error {
|
|
switch format {
|
|
case "json":
|
|
enc := json.NewEncoder(w)
|
|
enc.SetIndent("", " ")
|
|
return enc.Encode(rows)
|
|
case "csv":
|
|
fmt.Fprintln(w, "namespace,pod,cpu_req_m,cpu_use_m,cpu_lim_m,cpu_util,mem_req_bytes,mem_use_bytes,mem_lim_bytes,mem_util")
|
|
for _, r := range rows {
|
|
fmt.Fprintf(w, "%s,%s,%d,%d,%d,%.3f,%d,%d,%d,%.3f\n",
|
|
r.Namespace, r.Pod, r.CPUReqM, r.CPUUseM, r.CPULimM, r.cpuUtil(),
|
|
r.MemReqB, r.MemUseB, r.MemLimB, r.memUtil())
|
|
}
|
|
return nil
|
|
}
|
|
return fmt.Errorf("unknown output format %q", format)
|
|
}
|
|
|
|
// --- unit formatting ---
|
|
|
|
func cpu(m int64) string {
|
|
if m == 0 {
|
|
return "-"
|
|
}
|
|
if m < 0 {
|
|
return "-" + cpu(-m)
|
|
}
|
|
if m < 1000 {
|
|
return fmt.Sprintf("%dm", m)
|
|
}
|
|
return fmt.Sprintf("%.2f", float64(m)/1000)
|
|
}
|
|
|
|
func mem(b int64) string {
|
|
if b == 0 {
|
|
return "-"
|
|
}
|
|
if b < 0 {
|
|
return "-" + mem(-b)
|
|
}
|
|
const ki = 1024
|
|
switch {
|
|
case b >= ki*ki*ki:
|
|
return fmt.Sprintf("%.1fGi", float64(b)/(ki*ki*ki))
|
|
case b >= ki*ki:
|
|
return fmt.Sprintf("%.0fMi", float64(b)/(ki*ki))
|
|
case b >= ki:
|
|
return fmt.Sprintf("%.0fKi", float64(b)/ki)
|
|
default:
|
|
return fmt.Sprintf("%dB", b)
|
|
}
|
|
}
|