finish the main code

This commit is contained in:
Rainy21
2026-04-28 13:11:31 -04:00
commit 71a9858ddd
14 changed files with 234 additions and 0 deletions
View File
+13
View File
@@ -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()
+40
View File
@@ -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",
)
+15
View File
@@ -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()
+22
View File
@@ -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"}
+23
View File
@@ -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?"}
+31
View File
@@ -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
+31
View File
@@ -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)
View File
View File
+54
View File
@@ -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
+5
View File
@@ -0,0 +1,5 @@
fastapi
uvicorn
kubernetes
python-dotenv
httpx
View File
View File