internal/waste/waste.go
Rama main · réplica de lectura del repositorio original.
package waste
import (
"cmp"
"context"
"maps"
"slices"
"strconv"
"strings"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"github.com/sophotechlabs/spinoza/internal/api"
)
const (
basisPoints = 10000
namesShown = 3
noPods = "no pods are running here"
onlyFinished = "every pod here has finished, and a finished pod reserves nothing"
noSource = "no usage source is wired up"
notMeasured = "nothing measured usage, so this shows what is reserved and not what is used"
)
type Ref struct {
Namespace string
Name string
}
type Reading struct {
Pods map[string]api.ResourceUsage
Source string
Window string
}
type Meter interface {
Usage(ctx context.Context, pods []Ref) (Reading, error)
}
type Lister interface {
ListKind(ctx context.Context, ref api.ObjectRef) ([]*unstructured.Unstructured, error)
}
type Request struct {
Namespace string
Meters []Meter
}
type held struct {
ref Ref
kind string
owner string
cpu int64
mem int64
asks bool
used api.ResourceUsage
measured bool
}
func Build(ctx context.Context, lister Lister, req Request) (api.WasteReport, error) {
found, err := lister.ListKind(ctx, podsIn(req.Namespace))
if err != nil {
return api.WasteReport{}, err
}
if len(found) == 0 {
return api.WasteReport{Namespaces: []api.WasteRow{}, Workloads: []api.WasteRow{}, Reason: noPods}, nil
}
owners, notes := ownerIndex(ctx, lister, req.Namespace)
items := reservations(found, owners)
if len(items) == 0 {
return api.WasteReport{Namespaces: []api.WasteRow{}, Workloads: []api.WasteRow{}, Reason: onlyFinished}, nil
}
reading, measured, why := measure(ctx, req.Meters, refsOf(items))
attach(items, reading.Pods)
out := api.WasteReport{
Namespaces: ranked(rollup(items, namespaceRow)),
Workloads: ranked(rollup(items, workloadRow)),
Measured: measured,
}
if measured {
out.Window = reading.Window
out.Source = reading.Source
out.PartialOn = partialOn(items, out.Namespaces, out.Workloads)
}
out.Reason = reasonOf(measured, why, items, notes)
return out, nil
}
func podsIn(namespace string) api.ObjectRef {
return api.ObjectRef{Version: "v1", Resource: "pods", Namespace: namespace}
}
func reservations(found []*unstructured.Unstructured, owners map[string]*unstructured.Unstructured) []held {
out := make([]held, 0, len(found))
for _, obj := range found {
if finished(obj) {
continue
}
cpu, mem, asks := requestsOf(obj)
kind, name := topOwner(obj, owners)
out = append(out, held{
ref: Ref{Namespace: obj.GetNamespace(), Name: obj.GetName()},
kind: kind,
owner: name,
cpu: cpu,
mem: mem,
asks: asks,
})
}
return out
}
func finished(obj *unstructured.Unstructured) bool {
phase, _, err := unstructured.NestedString(obj.Object, "status", "phase")
if err != nil {
return false
}
return phase == "Succeeded" || phase == "Failed"
}
func refsOf(items []held) []Ref {
out := make([]Ref, 0, len(items))
for _, item := range items {
out = append(out, item.ref)
}
return out
}
func measure(ctx context.Context, meters []Meter, pods []Ref) (Reading, bool, string) {
troubles := make([]string, 0, len(meters))
for _, one := range meters {
read, err := one.Usage(ctx, pods)
if err == nil {
return read, true, ""
}
troubles = append(troubles, err.Error())
}
if len(troubles) == 0 {
return Reading{}, false, noSource
}
return Reading{}, false, strings.Join(troubles, "; ")
}
func attach(items []held, usage map[string]api.ResourceUsage) {
for at := range items {
use, ok := usage[items[at].ref.Namespace+"/"+items[at].ref.Name]
if !ok {
continue
}
items[at].used = use
items[at].measured = true
}
}
func namespaceRow(item held) api.WasteRow {
return api.WasteRow{Namespace: item.ref.Namespace}
}
func workloadRow(item held) api.WasteRow {
return api.WasteRow{Kind: item.kind, Namespace: item.ref.Namespace, Name: item.owner}
}
func rollup(items []held, keyOf func(held) api.WasteRow) []api.WasteRow {
place := map[string]int{}
rows := []api.WasteRow{}
whole := []bool{}
for _, item := range items {
base := keyOf(item)
id := base.Kind + "\x00" + base.Namespace + "\x00" + base.Name
where, seen := place[id]
if !seen {
where = len(rows)
place[id] = where
rows = append(rows, base)
whole = append(whole, true)
}
into := &rows[where]
into.Pods++
into.CPURequested += item.cpu
into.MemRequested += item.mem
if !item.measured {
whole[where] = false
continue
}
into.CPUUsed += item.used.CPUMilli
into.MemUsed += item.used.MemoryMi
into.CPUReclaimable += floorAtZero(item.cpu - item.used.CPUMilli)
into.MemReclaimable += floorAtZero(item.mem - item.used.MemoryMi)
}
for at := range rows {
rows[at].Measured = whole[at]
}
return rows
}
func floorAtZero(value int64) int64 {
if value < 0 {
return 0
}
return value
}
func ranked(rows []api.WasteRow) []api.WasteRow {
var cpuTotal, memTotal int64
for _, row := range rows {
cpuTotal += row.CPUReclaimable
memTotal += row.MemReclaimable
}
slices.SortStableFunc(rows, func(left, right api.WasteRow) int {
byShare := cmp.Compare(score(right, cpuTotal, memTotal), score(left, cpuTotal, memTotal))
if byShare != 0 {
return byShare
}
byNamespace := strings.Compare(left.Namespace, right.Namespace)
if byNamespace != 0 {
return byNamespace
}
return strings.Compare(left.Name, right.Name)
})
return rows
}
func score(row api.WasteRow, cpuTotal, memTotal int64) int64 {
return share(row.CPUReclaimable, cpuTotal) + share(row.MemReclaimable, memTotal)
}
func share(part, whole int64) int64 {
if whole <= 0 {
return 0
}
return part * basisPoints / whole
}
func partialOn(items []held, namespaces, workloads []api.WasteRow) []string {
seen := map[string]int{}
for _, item := range items {
if !item.measured {
continue
}
seen[item.ref.Namespace]++
}
names := map[string]bool{}
for _, row := range namespaces {
if row.Measured || seen[row.Namespace] > 0 {
continue
}
names[row.Namespace] = true
}
for _, row := range workloads {
if row.Measured || seen[row.Namespace] == 0 {
continue
}
names[row.Namespace+"/"+row.Name] = true
}
if len(names) == 0 {
return nil
}
return slices.Sorted(maps.Keys(names))
}
func reasonOf(measured bool, why string, items []held, notes []string) string {
parts := make([]string, 0, len(notes)+2)
if !measured {
parts = append(parts, notMeasured+": "+why)
}
bare := bareNames(items)
if len(bare) > 0 {
parts = append(parts, strconv.Itoa(len(bare))+" "+podWord(len(bare))+
" nothing and cannot be ranked: "+nameList(bare))
}
parts = append(parts, notes...)
return strings.Join(parts, "; ")
}
func bareNames(items []held) []string {
out := []string{}
for _, item := range items {
if item.asks {
continue
}
out = append(out, item.ref.Namespace+"/"+item.ref.Name)
}
return out
}
func podWord(count int) string {
if count == 1 {
return "pod reserves"
}
return "pods reserve"
}
func nameList(names []string) string {
slices.Sort(names)
if len(names) <= namesShown {
return strings.Join(names, ", ")
}
return strings.Join(names[:namesShown], ", ") +
", and " + strconv.Itoa(len(names)-namesShown) + " more"
}