my-homelab-configs/cmd/jeannie-core/main.go

266 lines
5.9 KiB
Go

package main
import (
"encoding/json"
"flag"
"fmt"
"io"
"os"
"sort"
"strings"
)
type reportRow struct {
Status string `json:"status"`
Area string `json:"area"`
Check string `json:"check"`
Summary string `json:"summary"`
Detail string `json:"detail"`
Fix string `json:"fix"`
Graph string `json:"graph"`
Query string `json:"query"`
}
func main() {
if len(os.Args) < 2 {
usage(os.Stderr)
os.Exit(2)
}
switch os.Args[1] {
case "report-render":
if err := runReportRender(os.Args[2:], os.Stdin, os.Stdout); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
case "help", "-h", "--help":
usage(os.Stdout)
default:
usage(os.Stderr)
os.Exit(2)
}
}
func usage(out io.Writer) {
fmt.Fprintln(out, "Usage: jeannie-core report-render [--title TITLE] [--details] [--only failures|warnings|problems|all] [--json]")
}
func runReportRender(args []string, in io.Reader, out io.Writer) error {
flags := flag.NewFlagSet("report-render", flag.ContinueOnError)
flags.SetOutput(io.Discard)
title := flags.String("title", envDefault("REPORT_TITLE", "Jeannie Report"), "")
details := flags.Bool("details", false, "")
only := flags.String("only", "all", "")
asJSON := flags.Bool("json", false, "")
if err := flags.Parse(args); err != nil {
usage(os.Stderr)
return err
}
rows, err := parseReportRows(in)
if err != nil {
return err
}
rows = filterReportRows(rows, *only)
if *asJSON {
encoder := json.NewEncoder(out)
encoder.SetIndent("", " ")
return encoder.Encode(map[string]any{"title": *title, "rows": rows})
}
renderReportText(out, *title, rows, *details, *only)
return nil
}
func envDefault(name, fallback string) string {
if value := os.Getenv(name); value != "" {
return value
}
return fallback
}
func parseReportRows(in io.Reader) ([]reportRow, error) {
data, err := io.ReadAll(in)
if err != nil {
return nil, err
}
var rows []reportRow
for _, raw := range strings.Split(string(data), "\n") {
raw = strings.TrimSuffix(raw, "\r")
if strings.TrimSpace(raw) == "" {
continue
}
parts := strings.Split(raw, "\t")
for len(parts) < 8 {
parts = append(parts, "")
}
row := reportRow{
Status: normalizeStatus(parts[0]),
Area: strings.TrimSpace(parts[1]),
Check: strings.TrimSpace(parts[2]),
Summary: strings.TrimSpace(parts[3]),
Detail: strings.TrimSpace(parts[4]),
Fix: strings.TrimSpace(parts[5]),
Graph: strings.TrimSpace(parts[6]),
Query: strings.TrimSpace(parts[7]),
}
if row.Area == "" {
row.Area = "General"
}
if row.Status == "ok" {
row.Graph = ""
row.Query = ""
}
rows = append(rows, row)
}
return rows, nil
}
func normalizeStatus(value string) string {
status := strings.ToLower(strings.TrimSpace(value))
if status == "" {
return "skip"
}
return status
}
func filterReportRows(rows []reportRow, only string) []reportRow {
var filtered []reportRow
for _, row := range rows {
if includeReportRow(row, only) {
filtered = append(filtered, row)
}
}
return filtered
}
func includeReportRow(row reportRow, only string) bool {
switch only {
case "all":
return true
case "failures":
return row.Status == "fail"
case "warnings":
return row.Status == "warn"
case "problems":
return row.Status == "fail" || row.Status == "warn"
default:
return true
}
}
func renderReportText(out io.Writer, title string, rows []reportRow, details bool, only string) {
counts := map[string]int{"fail": 0, "warn": 0, "ok": 0, "skip": 0}
for _, row := range rows {
counts[row.Status]++
}
fmt.Fprintln(out, title)
fmt.Fprintln(out, strings.Repeat("=", len(title)))
fmt.Fprintf(out, "fail=%d warn=%d ok=%d skip=%d\n", counts["fail"], counts["warn"], counts["ok"], counts["skip"])
if len(rows) == 0 {
switch only {
case "problems":
fmt.Fprintln(out, "\nNo failing or warning items.")
case "failures":
fmt.Fprintln(out, "\nNo failing items.")
case "warnings":
fmt.Fprintln(out, "\nNo warning items.")
default:
fmt.Fprintln(out, "\nNo report rows.")
}
return
}
for _, area := range orderedReportAreas(rows) {
fmt.Fprintf(out, "\n== %s ==\n", area)
areaRows := rowsForReportArea(rows, area)
sort.SliceStable(areaRows, func(i, j int) bool {
leftRank := reportStatusRank(areaRows[i].Status)
rightRank := reportStatusRank(areaRows[j].Status)
if leftRank != rightRank {
return leftRank < rightRank
}
return areaRows[i].Check < areaRows[j].Check
})
for _, row := range areaRows {
fmt.Fprintf(out, "%-5s %-30s %s\n", row.Status, truncate(row.Check, 30), row.Summary)
if details || row.Status == "fail" || row.Status == "warn" {
if row.Detail != "" {
fmt.Fprintf(out, " detail: %s\n", row.Detail)
}
if row.Fix != "" {
fmt.Fprintf(out, " fix: %s\n", row.Fix)
}
if row.Graph != "" {
fmt.Fprintf(out, " graph: %s\n", row.Graph)
}
if row.Query != "" {
fmt.Fprintf(out, " query: %s\n", row.Query)
}
}
}
}
for _, row := range rows {
if row.Status == "fail" || row.Status == "warn" {
fmt.Fprintln(out, "\nNext Fix")
if row.Fix != "" {
fmt.Fprintln(out, row.Fix)
} else {
fmt.Fprintf(out, "Investigate %s / %s\n", row.Area, row.Check)
}
return
}
}
}
func orderedReportAreas(rows []reportRow) []string {
seen := map[string]bool{}
var areas []string
for _, row := range rows {
if !seen[row.Area] {
seen[row.Area] = true
areas = append(areas, row.Area)
}
}
return areas
}
func rowsForReportArea(rows []reportRow, area string) []reportRow {
var areaRows []reportRow
for _, row := range rows {
if row.Area == area {
areaRows = append(areaRows, row)
}
}
return areaRows
}
func reportStatusRank(status string) int {
switch status {
case "fail":
return 0
case "warn":
return 1
case "skip":
return 2
case "ok":
return 3
default:
return 9
}
}
func truncate(value string, max int) string {
if len(value) <= max {
return value
}
return value[:max]
}