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) } }