Start Jeannie Go core migration
This commit is contained in:
parent
44d43f2b42
commit
d63e402012
|
|
@ -3,6 +3,7 @@
|
|||
*.tfstate.backup
|
||||
.terraform/
|
||||
.lab/
|
||||
bin/jeannie-core
|
||||
bootstrap/host/inventory/
|
||||
|
||||
# Ignore local archive dumps and backups
|
||||
|
|
|
|||
13
README.md
13
README.md
|
|
@ -110,6 +110,8 @@ On the Debian host:
|
|||
minor, currently `v1.36`
|
||||
- SSH access to worker nodes
|
||||
- SSH access to the OCI edge host
|
||||
- Go toolchain for Jeannie core helpers; `./jeannie up` installs
|
||||
`golang-go` on the Debian host if it is missing
|
||||
- enough persistent storage for `/data/openebs/local` and Docker's data root
|
||||
|
||||
After a clean Debian reinstall, bootstrap the host package set and desktop
|
||||
|
|
@ -130,6 +132,17 @@ Details are in `bootstrap/host/README.md`.
|
|||
The default kubeconfig path is `/home/jv/.kube/config`. Override it with
|
||||
`KUBECONFIG_PATH` or `TF_VAR_kubeconfig_path` when needed.
|
||||
|
||||
## Implementation Language Policy
|
||||
|
||||
Jeannie keeps `./jeannie` as the stable command entrypoint. New durable
|
||||
read-only orchestration, report rendering, inventory parsing, status/doctor
|
||||
logic, and self-heal planning should prefer Go under `cmd/jeannie-core` because
|
||||
it ships as one small binary and is easier to test than Bash. Bash remains for
|
||||
thin compatibility wrappers and remote shell glue. Python remains acceptable for
|
||||
AI/RAG, data-heavy utilities, or cases where an existing Python ecosystem is the
|
||||
clear fit. New repo features should choose the smallest language that fits the
|
||||
task instead of defaulting to Bash.
|
||||
|
||||
## Version Discipline
|
||||
|
||||
The lab should converge on one Kubernetes minor across the API server and every
|
||||
|
|
|
|||
|
|
@ -110,6 +110,8 @@ On the Debian host:
|
|||
minor, currently `v1.36`
|
||||
- SSH access to worker nodes
|
||||
- SSH access to the OCI edge host
|
||||
- Go toolchain for Jeannie core helpers; `./{{ main_script }} up` installs
|
||||
`golang-go` on the Debian host if it is missing
|
||||
- enough persistent storage for `/data/openebs/local` and Docker's data root
|
||||
|
||||
After a clean Debian reinstall, bootstrap the host package set and desktop
|
||||
|
|
@ -130,6 +132,17 @@ Details are in `bootstrap/host/README.md`.
|
|||
The default kubeconfig path is `/home/jv/.kube/config`. Override it with
|
||||
`KUBECONFIG_PATH` or `TF_VAR_kubeconfig_path` when needed.
|
||||
|
||||
## Implementation Language Policy
|
||||
|
||||
Jeannie keeps `./{{ main_script }}` as the stable command entrypoint. New durable
|
||||
read-only orchestration, report rendering, inventory parsing, status/doctor
|
||||
logic, and self-heal planning should prefer Go under `cmd/jeannie-core` because
|
||||
it ships as one small binary and is easier to test than Bash. Bash remains for
|
||||
thin compatibility wrappers and remote shell glue. Python remains acceptable for
|
||||
AI/RAG, data-heavy utilities, or cases where an existing Python ecosystem is the
|
||||
clear fit. New repo features should choose the smallest language that fits the
|
||||
task instead of defaulting to Bash.
|
||||
|
||||
## Version Discipline
|
||||
|
||||
The lab should converge on one Kubernetes minor across the API server and every
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ debian_pc_base_packages:
|
|||
- curl
|
||||
- ffmpeg
|
||||
- git
|
||||
- golang-go
|
||||
- gnupg
|
||||
- gpg
|
||||
- lynis
|
||||
|
|
|
|||
|
|
@ -0,0 +1,265 @@
|
|||
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]
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestReportRenderProblemsOnly(t *testing.T) {
|
||||
input := strings.Join([]string{
|
||||
"ok\tArea\tHealthy\tok\t\t\tgraph\tquery",
|
||||
"warn\tArea\tWarning\tneeds attention\tdetail\tfix\tgraph\tquery",
|
||||
"",
|
||||
}, "\n")
|
||||
|
||||
var output bytes.Buffer
|
||||
if err := runReportRender([]string{"--title", "Unit Report", "--only", "problems"}, strings.NewReader(input), &output); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
got := output.String()
|
||||
if strings.Contains(got, "Healthy") {
|
||||
t.Fatalf("problems-only report included ok row:\n%s", got)
|
||||
}
|
||||
for _, want := range []string{"Unit Report", "warn=1", "Warning", "Next Fix", "fix"} {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Fatalf("report missing %q:\n%s", want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestReportRenderHidesOKGraphAndQuery(t *testing.T) {
|
||||
input := "ok\tArea\tHealthy\tok\tdetail\tfix\tgraph\tquery\n"
|
||||
|
||||
var output bytes.Buffer
|
||||
if err := runReportRender([]string{"--title", "Unit Report", "--only", "all", "--details"}, strings.NewReader(input), &output); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
got := output.String()
|
||||
if strings.Contains(got, "graph:") || strings.Contains(got, "query:") {
|
||||
t.Fatalf("ok row leaked graph/query links:\n%s", got)
|
||||
}
|
||||
}
|
||||
|
|
@ -487,6 +487,10 @@ full output is written to the log file.
|
|||
`10`. Compact steps show elapsed time and the latest sanitized log line while
|
||||
the full output continues writing to the log file.
|
||||
|
||||
`JEANNIE_CORE_BIN`
|
||||
: Optional path to a prebuilt Go `jeannie-core` binary. Defaults to
|
||||
`.lab/bin/jeannie-core`; wrappers build it on demand when Go is available.
|
||||
|
||||
`LAB_STATUS_DETAILS=true`
|
||||
: With `status --verbose`, append detailed host, Docker, Kubernetes, Pimox, RPi,
|
||||
Tailscale, and HTTP tables after the normal status cascade.
|
||||
|
|
|
|||
20
jeannie
20
jeannie
|
|
@ -954,6 +954,23 @@ ensure_python3() {
|
|||
sudo apt-get install -y --no-install-recommends python3
|
||||
}
|
||||
|
||||
ensure_go_toolchain() {
|
||||
require_debian_server "go toolchain"
|
||||
|
||||
if command -v go >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "Installing Go toolchain for Jeannie core helpers..."
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --no-install-recommends golang-go
|
||||
|
||||
if ! command -v go >/dev/null 2>&1; then
|
||||
echo "go is still unavailable after installing golang-go." >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
detect_route_interface() {
|
||||
local target="$1"
|
||||
|
||||
|
|
@ -4288,9 +4305,10 @@ up() {
|
|||
|
||||
echo "Deploying the homelab infrastructure..."
|
||||
jeannie_log_start "up"
|
||||
jeannie_step_plan 15
|
||||
jeannie_step_plan 16
|
||||
|
||||
run_step "Early preflight" homelab_preflight early
|
||||
run_step "Go toolchain" ensure_go_toolchain
|
||||
run_step "Gitea deploy" deploy_gitea
|
||||
run_step "Gitea repo bootstrap" bootstrap_gitea_repo
|
||||
run_step "RPi services" deploy_rpi_services
|
||||
|
|
|
|||
|
|
@ -1,20 +1,22 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
MODE="compact"
|
||||
ONLY="all"
|
||||
TITLE="${REPORT_TITLE:-Jeannie Report}"
|
||||
INPUT_FILE=""
|
||||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
CORE_BIN="${JEANNIE_CORE_BIN:-${REPO_ROOT}/.lab/bin/jeannie-core}"
|
||||
|
||||
cleanup() {
|
||||
if [ -n "$INPUT_FILE" ] && [ -f "$INPUT_FILE" ]; then
|
||||
rm -f "$INPUT_FILE"
|
||||
core_sources_newer() {
|
||||
local source
|
||||
|
||||
while IFS= read -r source; do
|
||||
if [[ "${source}" -nt "${CORE_BIN}" ]]; then
|
||||
return 0
|
||||
fi
|
||||
done < <(find "${REPO_ROOT}/cmd/jeannie-core" -name '*.go' -type f)
|
||||
return 1
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
cat <<'EOF'
|
||||
Usage: report-render [--title TITLE] [--details] [--only failures|warnings|problems|all] [--json]
|
||||
|
||||
Reads TSV rows from stdin:
|
||||
|
|
@ -24,144 +26,22 @@ Statuses: ok, warn, fail, skip
|
|||
EOF
|
||||
}
|
||||
|
||||
while (($# > 0)); do
|
||||
if (($# > 0)); then
|
||||
case "$1" in
|
||||
--title)
|
||||
TITLE="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--details)
|
||||
MODE="details"
|
||||
shift
|
||||
;;
|
||||
--only)
|
||||
ONLY="${2:-all}"
|
||||
shift 2
|
||||
;;
|
||||
--json)
|
||||
MODE="json"
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
-h | --help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
usage >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
fi
|
||||
|
||||
INPUT_FILE="$(mktemp "${TMPDIR:-/tmp}/jeannie-report.XXXXXX")"
|
||||
cat >"$INPUT_FILE"
|
||||
if [[ ! -x "${CORE_BIN}" || "${REPO_ROOT}/go.mod" -nt "${CORE_BIN}" ]] || core_sources_newer; then
|
||||
if ! command -v go >/dev/null 2>&1; then
|
||||
echo "go is required to build ${CORE_BIN}; run ./jeannie up or install golang-go." >&2
|
||||
exit 1
|
||||
fi
|
||||
mkdir -p "$(dirname "${CORE_BIN}")"
|
||||
GOCACHE="${GOCACHE:-${REPO_ROOT}/.lab/go-build-cache}" go build -o "${CORE_BIN}" "${REPO_ROOT}/cmd/jeannie-core"
|
||||
fi
|
||||
|
||||
python3 - "$TITLE" "$MODE" "$ONLY" "$INPUT_FILE" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
from collections import OrderedDict
|
||||
|
||||
title, mode, only, input_file = sys.argv[1:5]
|
||||
rows = []
|
||||
|
||||
with open(input_file, encoding="utf-8") as handle:
|
||||
for raw in handle:
|
||||
raw = raw.rstrip("\n")
|
||||
if not raw:
|
||||
continue
|
||||
parts = raw.split("\t")
|
||||
while len(parts) < 8:
|
||||
parts.append("")
|
||||
status, area, check, summary, detail, fix, graph, query = parts[:8]
|
||||
status = status.strip().lower() or "skip"
|
||||
if status == "ok":
|
||||
graph = ""
|
||||
query = ""
|
||||
rows.append(
|
||||
{
|
||||
"status": status,
|
||||
"area": area.strip() or "General",
|
||||
"check": check.strip(),
|
||||
"summary": summary.strip(),
|
||||
"detail": detail.strip(),
|
||||
"fix": fix.strip(),
|
||||
"graph": graph.strip(),
|
||||
"query": query.strip(),
|
||||
}
|
||||
)
|
||||
|
||||
def include(row):
|
||||
status = row["status"]
|
||||
if only == "all":
|
||||
return True
|
||||
if only == "failures":
|
||||
return status == "fail"
|
||||
if only == "warnings":
|
||||
return status == "warn"
|
||||
if only == "problems":
|
||||
return status in {"fail", "warn"}
|
||||
return True
|
||||
|
||||
rows = [row for row in rows if include(row)]
|
||||
|
||||
if mode == "json":
|
||||
print(json.dumps({"title": title, "rows": rows}, indent=2))
|
||||
raise SystemExit(0)
|
||||
|
||||
counts = {"fail": 0, "warn": 0, "ok": 0, "skip": 0}
|
||||
for row in rows:
|
||||
counts[row["status"]] = counts.get(row["status"], 0) + 1
|
||||
|
||||
print(title)
|
||||
print("=" * len(title))
|
||||
print(
|
||||
f"fail={counts.get('fail', 0)} "
|
||||
f"warn={counts.get('warn', 0)} "
|
||||
f"ok={counts.get('ok', 0)} "
|
||||
f"skip={counts.get('skip', 0)}"
|
||||
)
|
||||
|
||||
if not rows:
|
||||
if only == "problems":
|
||||
print("\nNo failing or warning items.")
|
||||
elif only == "failures":
|
||||
print("\nNo failing items.")
|
||||
elif only == "warnings":
|
||||
print("\nNo warning items.")
|
||||
else:
|
||||
print("\nNo report rows.")
|
||||
raise SystemExit(0)
|
||||
|
||||
groups = OrderedDict()
|
||||
for row in rows:
|
||||
groups.setdefault(row["area"], []).append(row)
|
||||
|
||||
status_rank = {"fail": 0, "warn": 1, "skip": 2, "ok": 3}
|
||||
status_label = {"fail": "fail", "warn": "warn", "ok": "ok", "skip": "skip"}
|
||||
|
||||
for area, area_rows in groups.items():
|
||||
print(f"\n== {area} ==")
|
||||
for row in sorted(area_rows, key=lambda item: (status_rank.get(item["status"], 9), item["check"])):
|
||||
status = status_label.get(row["status"], row["status"])
|
||||
check = row["check"][:30]
|
||||
summary = row["summary"]
|
||||
print(f"{status:<5} {check:<30} {summary}")
|
||||
if mode == "details" or row["status"] in {"fail", "warn"}:
|
||||
if row["detail"]:
|
||||
print(f" detail: {row['detail']}")
|
||||
if row["fix"]:
|
||||
print(f" fix: {row['fix']}")
|
||||
if row["graph"]:
|
||||
print(f" graph: {row['graph']}")
|
||||
if row["query"]:
|
||||
print(f" query: {row['query']}")
|
||||
|
||||
problems = [row for row in rows if row["status"] in {"fail", "warn"}]
|
||||
if problems:
|
||||
first = problems[0]
|
||||
print("\nNext Fix")
|
||||
if first["fix"]:
|
||||
print(first["fix"])
|
||||
else:
|
||||
print(f"Investigate {first['area']} / {first['check']}")
|
||||
PY
|
||||
exec "${CORE_BIN}" report-render "$@"
|
||||
|
|
|
|||
|
|
@ -78,6 +78,15 @@ validate_tofu_fmt() {
|
|||
tofu fmt -check -recursive "${REPO_ROOT}/bootstrap"
|
||||
}
|
||||
|
||||
validate_go() {
|
||||
if ! command -v go >/dev/null 2>&1; then
|
||||
echo "go not installed; skipping Go checks"
|
||||
return 0
|
||||
fi
|
||||
test -z "$(gofmt -l "${REPO_ROOT}/cmd")"
|
||||
GOCACHE="${GOCACHE:-${REPO_ROOT}/.lab/go-build-cache}" go test ./...
|
||||
}
|
||||
|
||||
validate_security_static() {
|
||||
"${REPO_ROOT}/scripts/validate-tailnet-policy"
|
||||
if command -v trivy >/dev/null 2>&1; then
|
||||
|
|
@ -99,6 +108,7 @@ main() {
|
|||
run_step "Bash syntax" validate_bash_syntax
|
||||
run_step "Shellcheck" validate_shellcheck
|
||||
run_step "YAML parse" validate_yaml
|
||||
run_step "Go checks" validate_go
|
||||
run_step "OpenTofu formatting" validate_tofu_fmt
|
||||
run_step "Security static checks" validate_security_static
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue