Initial benvin-utils monorepo with podgap tool

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
This commit is contained in:
Ben Vin
2026-07-12 21:42:14 +10:00
committed by Ben Vincent
parent e6bffb35ac
commit 016f93ab4f
12 changed files with 1221 additions and 1 deletions
+54
View File
@@ -0,0 +1,54 @@
// Package kube provides shared Kubernetes client bootstrapping for the tools in
// benvin-utils: a single loader that works both in-cluster and from a local
// kubeconfig, plus a helper that returns the core and metrics clients together.
package kube
import (
"fmt"
"os"
"path/filepath"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
metricsv "k8s.io/metrics/pkg/client/clientset/versioned"
)
// LoadConfig resolves a *rest.Config, preferring in-cluster credentials, then
// the explicit path, then $KUBECONFIG, then ~/.kube/config.
func LoadConfig(explicit string) (*rest.Config, error) {
if cfg, err := rest.InClusterConfig(); err == nil {
return cfg, nil
}
path := explicit
if path == "" {
path = os.Getenv("KUBECONFIG")
}
if path == "" {
if home, err := os.UserHomeDir(); err == nil {
path = filepath.Join(home, ".kube", "config")
}
}
cfg, err := clientcmd.BuildConfigFromFlags("", path)
if err != nil {
return nil, fmt.Errorf("load kubeconfig %q: %w", path, err)
}
return cfg, nil
}
// Clients returns the core and metrics clientsets for the resolved config.
func Clients(explicit string) (kubernetes.Interface, metricsv.Interface, error) {
cfg, err := LoadConfig(explicit)
if err != nil {
return nil, nil, err
}
core, err := kubernetes.NewForConfig(cfg)
if err != nil {
return nil, nil, fmt.Errorf("core client: %w", err)
}
mc, err := metricsv.NewForConfig(cfg)
if err != nil {
return nil, nil, fmt.Errorf("metrics client: %w", err)
}
return core, mc, nil
}
+86
View File
@@ -0,0 +1,86 @@
// Package tui holds small, reusable terminal-UI primitives shared across the
// tools in benvin-utils: direction markers, a common style palette, and
// width-aware padding/truncation helpers that respect ANSI styling.
package tui
import (
"strings"
"github.com/charmbracelet/lipgloss"
)
// Shared style palette. Tools should prefer these so every benvin-utils TUI
// looks consistent.
var (
Header = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("15")).Background(lipgloss.Color("240"))
Title = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("87"))
Dim = lipgloss.NewStyle().Foreground(lipgloss.Color("244"))
Up = lipgloss.NewStyle().Foreground(lipgloss.Color("203")) // a rising value = red-ish
Down = lipgloss.NewStyle().Foreground(lipgloss.Color("84")) // a falling value = green
Hot = lipgloss.NewStyle().Foreground(lipgloss.Color("203")) // over threshold
Warn = lipgloss.NewStyle().Foreground(lipgloss.Color("214")) // approaching threshold
Err = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("203"))
)
// Marker glyphs for direction-of-change indicators.
const (
GlyphUp = "▲"
GlyphDown = "▼"
GlyphFlat = "·"
)
// Arrow renders a direction marker: 1 = up (▲), -1 = down (▼), 0 = flat (·).
func Arrow(dir int) string {
switch {
case dir > 0:
return Up.Render(GlyphUp)
case dir < 0:
return Down.Render(GlyphDown)
default:
return Dim.Render(GlyphFlat)
}
}
// Sign reports whether d is positive (1), negative (-1), or zero (0). It is the
// usual companion to Arrow when comparing a new sample against a previous one.
func Sign[T int | int64 | float64](d T) int {
switch {
case d > 0:
return 1
case d < 0:
return -1
default:
return 0
}
}
// Pad right-pads plain (unstyled) text to width w, truncating if longer.
func Pad(s string, w int) string {
if len(s) > w {
s = s[:w]
}
return s + strings.Repeat(" ", w-len(s))
}
// PadVisible right-pads a possibly ANSI-styled cell to width w using its visible
// (rendered) width, so styled cells still align in a table.
func PadVisible(s string, w int) string {
if vis := lipgloss.Width(s); vis < w {
return s + strings.Repeat(" ", w-vis)
}
return s
}
// Truncate shortens s to at most n runes, adding an ellipsis when it cuts.
func Truncate(s string, n int) string {
if n <= 0 {
return ""
}
if len(s) <= n {
return s
}
if n == 1 {
return "…"
}
return s[:n-1] + "…"
}