From 71a9858ddd87c1ff50fbad27a42cd483e37dc971 Mon Sep 17 00:00:00 2001 From: Rainy21 Date: Tue, 28 Apr 2026 13:11:31 -0400 Subject: [PATCH] finish the main code --- app.py | 0 app/__init__.py | 13 +++++++++ app/config.py | 40 ++++++++++++++++++++++++++++ app/k8s_client.py | 15 +++++++++++ app/main.py | 22 ++++++++++++++++ app/routers/metrics.py | 23 ++++++++++++++++ app/routers/nodes.py | 31 ++++++++++++++++++++++ app/routers/pods.py | 31 ++++++++++++++++++++++ dashboard/index.html | 0 dashboard/static/style.css | 0 k3s-dashboard.yaml | 54 ++++++++++++++++++++++++++++++++++++++ requirements.txt | 5 ++++ tests/test_nodes.py | 0 tests/test_pods.py | 0 14 files changed, 234 insertions(+) create mode 100644 app.py create mode 100644 app/__init__.py create mode 100644 app/config.py create mode 100644 app/k8s_client.py create mode 100644 app/main.py create mode 100644 app/routers/metrics.py create mode 100644 app/routers/nodes.py create mode 100644 app/routers/pods.py create mode 100644 dashboard/index.html create mode 100644 dashboard/static/style.css create mode 100644 k3s-dashboard.yaml create mode 100644 requirements.txt create mode 100644 tests/test_nodes.py create mode 100644 tests/test_pods.py diff --git a/app.py b/app.py new file mode 100644 index 0000000..e69de29 diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..81bf4b7 --- /dev/null +++ b/app/__init__.py @@ -0,0 +1,13 @@ +""" +app/__init__.py + +Makes `app/` a Python package and exposes a single shared Config instance. + +Usage anywhere in the project: + from app import cfg + print(cfg.cache_ttl) +""" + +from app.config import load + +cfg = load() diff --git a/app/config.py b/app/config.py new file mode 100644 index 0000000..9e994e1 --- /dev/null +++ b/app/config.py @@ -0,0 +1,40 @@ +""" +app/config.py + +Central place for all runtime configuration. +Values are read from environment variables, with sensible defaults. +Nothing in this file imports from the rest of the project. +""" + +import os +from dataclasses import dataclass + + +@dataclass(frozen=True) +class Config: + # Kubernetes connection + in_cluster: bool # True when running as a pod inside k3s + kubeconfig: str # path to kubeconfig file (ignored if in_cluster=True) + + # Dashboard behaviour + cache_ttl: int # seconds between Kubernetes API refreshes + page_title: str + page_icon: str + + # Metrics + metrics_enabled: bool # set False if metrics-server is not installed + + +def load() -> Config: + """ + Read config from the environment. + Call once at app startup — the result is immutable. + """ + return Config( + in_cluster=os.getenv("IN_CLUSTER", "").lower() in ("1", "true", "yes"), + kubeconfig=os.getenv("KUBECONFIG", os.path.expanduser("~/.kube/config")), + cache_ttl=int(os.getenv("CACHE_TTL", "15")), + page_title=os.getenv("PAGE_TITLE", "k3s Dashboard"), + page_icon=os.getenv("PAGE_ICON", "☸️"), + metrics_enabled=os.getenv("METRICS_ENABLED", "true").lower() != "false", + ) diff --git a/app/k8s_client.py b/app/k8s_client.py new file mode 100644 index 0000000..7122952 --- /dev/null +++ b/app/k8s_client.py @@ -0,0 +1,15 @@ +import os +import kubernetes import client, config + +def get_core_client() -> client.CoreV1Api: + if os.getenv("IN_CLUSTER"): + config.load_incluster_config() + else: + config.load_kube_config( # runs locally + config_file=os.getenv("KUBECONFIG", "~/.kube/config") + ) + return client.CoreV1Api() + +def get_custom_client() -> client.CustomObjectsApi: + return client.CustomObjectsApi() + diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000..d678640 --- /dev/null +++ b/app/main.py @@ -0,0 +1,22 @@ +from fastapi import FastAPI +from fastapi.staticfiles import StaticFiles +from fastapi.responses import FileResponse +from app.routers import nodes, pods, metrics + +app = FastAPI(title="k3s Health Dashboard") + +app.include_router(nodes.router) +app.include_router(pods.router) +app.include_router(metrics.router) + +app.mount("/static", StaticFiles(directory="dashboard/static"), name="static") + + +@app.get("/") +def serve_ui(): + return FileResponse("dashboard/index.html") + + +@app.get("/health") +def health(): + return {"status": "ok"} diff --git a/app/routers/metrics.py b/app/routers/metrics.py new file mode 100644 index 0000000..575dd87 --- /dev/null +++ b/app/routers/metrics.py @@ -0,0 +1,23 @@ +from fastapi import APIRouter +from k8s_client import get_custom_client + +router = APIRouter(prefix="/metrics", tags=["metrics"]) + + +@router.get("/nodes") +def node_metrics(): + api = get_custom_client() + try: + data = api.list_cluster_custom_object( + group="metrics.k8s.io", version="v1beta1", plural="nodes" + ) + return [ + { + "name": item["metadata"]["name"], + "cpu_usage": item["usage"]["cpu"], # e.g. "250m" + "memory_usage": item["usage"]["memory"], # e.g. "512Mi" + } + for item in data["items"] + ] + except Exception as e: + return {"error": str(e), "hint": "Is metrics-server installed?"} diff --git a/app/routers/nodes.py b/app/routers/nodes.py new file mode 100644 index 0000000..88593b1 --- /dev/null +++ b/app/routers/nodes.py @@ -0,0 +1,31 @@ +from fastapi import APIRouter +from app.k8s_client import get_core_client + +router = APIRouter(prefix="/nodes", tags=["nodes"]) + + +@router.get("/") +def list_node(): + v1 = get_core_client() + nodes = v1.list_node().items + result = [] + for n in nodes: + conditions = {c.type: c.status for c in n.status.conditions} + result.append( + { + "name": n.metadata.name, + "ready": conditions.get("Ready") == "True", + "cpu_capacity": n.status.capacity.get("cpu"), + "memory_capacity": n.status.capacity.get("memory"), + "labels": n.metadata.labels, + "age_days": _age(n.metadata.creation_timestamp), + } + ) + return result + + +def _age(ts) -> int: + from datetime import datetime, timezone + + now = datetime.now(timezone.utc) + return (now - ts).days diff --git a/app/routers/pods.py b/app/routers/pods.py new file mode 100644 index 0000000..948ca7c --- /dev/null +++ b/app/routers/pods.py @@ -0,0 +1,31 @@ +from fastapi import APIRouter, Query +from k8s_client import get_core_client + +router = APIRouter(prefix="/pods", tags=["pods"]) + +@router.get("/") +def list_pods(namespace: str = Query("default")): + v1 = get_core_client() + pods = v1.list_namespaced_pod(namespace=namespace).items + return [ + { + "name": p.metadata.name, + "namespace": p.metadata.namespace, + "phase": p.status.phase, # Pending/Running/Succeeded/Failed + "ready": _is_ready(p), + "restarts": _restarts(p), + "node": p.spec.node_name, + } + for p in pods + ] + + +def _is_ready(pod) -> bool : + if not pod.status.conditions: + return False + for any(c.type == "Ready" and c.status == "True" for c in pod.status.conditions) + +def _restarts(pod) -> int: + if not pod.status.container_statuses: + return 0 + return sum(c.restart_count for c in pod.status.container_statuses) diff --git a/dashboard/index.html b/dashboard/index.html new file mode 100644 index 0000000..e69de29 diff --git a/dashboard/static/style.css b/dashboard/static/style.css new file mode 100644 index 0000000..e69de29 diff --git a/k3s-dashboard.yaml b/k3s-dashboard.yaml new file mode 100644 index 0000000..b87d75b --- /dev/null +++ b/k3s-dashboard.yaml @@ -0,0 +1,54 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: dashboard-sa + namespace: default +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: dashboard-reader +rules: + - apiGroups: [""] + resources: ["nodes", "pods", "namespaces", "events"] + verbs: ["get", "list", "watch"] + - apiGroups: ["metrics.k8s.io"] + resources: ["nodes", "pods"] + verbs: ["get", "list"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: dashboard-reader-binding +subjects: + - kind: ServiceAccount + name: dashboard-sa + namespace: default +roleRef: + kind: ClusterRole + name: dashboard-reader + apiGroup: rbac.authorization.k8s.io +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: k3s-dashboard +spec: + replicas: 1 + selector: + matchLabels: + app: k3s-dashboard + template: + metadata: + labels: + app: k3s-dashboard + spec: + serviceAccountName: dashboard-sa + containers: + - name: dashboard + image: your-registry/k3s-dashboard:latest + env: + - name: IN_CLUSTER + value: "true" + ports: + - containerPort: 8080 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..cce9705 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +fastapi +uvicorn +kubernetes +python-dotenv +httpx diff --git a/tests/test_nodes.py b/tests/test_nodes.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_pods.py b/tests/test_pods.py new file mode 100644 index 0000000..e69de29