Files
kubectl-gui/backend/main.py
T
Jiří Stachandaider 77febb993e feat: add backend caching and pagination for resources
Co-authored-by: aider (openrouter/thinkingmachines/inkling-small:free) <aider@aider.chat>
2026-10-09 18:01:48 +02:00

179 lines
6.7 KiB
Python

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
import subprocess
import time
import json
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
cache = {}
CACHE_TTL = 300 # 5 minutes
def run_kubectl(args, context=None):
cmd = ["kubectl"]
if context:
cmd += ["--context", context]
cmd += args
result = subprocess.run(cmd, capture_output=True, text=True)
return result
def get_cache_key(resource, namespace, context):
return f"{resource}:{namespace}:{context or ''}"
def get_cached_items(resource, namespace, context):
key = get_cache_key(resource, namespace, context)
if key in cache:
ts, items = cache[key]
if time.time() - ts < CACHE_TTL:
return items
return None
def set_cached_items(resource, namespace, context, items):
key = get_cache_key(resource, namespace, context)
cache[key] = (time.time(), items)
@app.get("/")
def root():
return {"message": "Kubectl Web GUI backend"}
@app.get("/contexts")
def get_contexts():
result = run_kubectl(["config", "get-contexts", "-o", "name"])
contexts = [line for line in result.stdout.splitlines() if line]
current = ""
try:
cur = run_kubectl(["config", "current-context"])
current = cur.stdout.strip()
except Exception:
pass
return {"current": current, "contexts": contexts}
@app.get("/namespaces")
def get_namespaces(context: str = None):
result = run_kubectl(
["get", "namespaces", "-o", "jsonpath={range .items[*]}{.metadata.name}{\"\\n\"}{end}"],
context=context
)
namespaces = [line for line in result.stdout.splitlines() if line]
return {"namespaces": namespaces}
def paginated_list(resource, namespace, context, search, page, page_size, bypass, kubectl_args, item_mapper):
key = get_cache_key(resource, namespace, context)
items = None
if not bypass:
items = get_cached_items(resource, namespace, context)
if items is None:
result = run_kubectl(kubectl_args, context=context)
try:
obj = json.loads(result.stdout)
raw_items = obj.get("items", [])
except Exception:
raw_items = []
items = [item_mapper(i) for i in raw_items]
set_cached_items(resource, namespace, context, items)
if search:
s = search.lower()
items = [i for i in items if s in (i.get("name") or "").lower()]
total = len(items)
start = (page - 1) * page_size
paginated = items[start:start + page_size]
return {
"items": paginated,
"total": total,
"page": page,
"page_size": page_size
}
@app.get("/pods")
def get_pods(namespace: str = "default", context: str = None, search: str = "", page: int = 1, page_size: int = 10, bypass: bool = False):
def mapper(i):
return {
"name": i.get("metadata", {}).get("name", "unknown"),
"namespace": i.get("metadata", {}).get("namespace", namespace)
}
return paginated_list("pods", namespace, context, search, page, page_size, bypass, ["get", "pods", "-n", namespace, "-o", "json"], mapper)
@app.get("/nodes")
def get_nodes(context: str = None, search: str = "", page: int = 1, page_size: int = 10, bypass: bool = False):
def mapper(i):
return {
"name": i.get("metadata", {}).get("name", "unknown"),
"namespace": "-"
}
return paginated_list("nodes", "default", context, search, page, page_size, bypass, ["get", "nodes", "-o", "json"], mapper)
@app.get("/pvcs")
def get_pvcs(namespace: str = "default", context: str = None, search: str = "", page: int = 1, page_size: int = 10, bypass: bool = False):
def mapper(i):
return {
"name": i.get("metadata", {}).get("name", "unknown"),
"namespace": i.get("metadata", {}).get("namespace", namespace)
}
return paginated_list("pvcs", namespace, context, search, page, page_size, bypass, ["get", "pvc", "-n", namespace, "-o", "json"], mapper)
@app.get("/configmaps")
def get_configmaps(namespace: str = "default", context: str = None, search: str = "", page: int = 1, page_size: int = 10, bypass: bool = False):
def mapper(i):
return {
"name": i.get("metadata", {}).get("name", "unknown"),
"namespace": i.get("metadata", {}).get("namespace", namespace)
}
return paginated_list("configmaps", namespace, context, search, page, page_size, bypass, ["get", "configmaps", "-n", namespace, "-o", "json"], mapper)
@app.get("/describe")
def describe(resource: str, name: str, namespace: str = "default", context: str = None):
allowed = {"pod", "node", "pvc", "service", "deployment", "configmap"}
if resource not in allowed:
return {"error": "Invalid resource type"}
result = run_kubectl(
["describe", resource, name, "-n", namespace],
context=context
)
return {"output": result.stdout, "stderr": result.stderr, "returncode": result.returncode}
@app.post("/delete")
def delete(resource: str, name: str, namespace: str = "default", context: str = None):
allowed = {"pod", "node", "pvc", "service", "deployment", "configmap"}
if resource not in allowed:
return {"error": "Invalid resource type"}
result = run_kubectl(
["delete", resource, name, "-n", namespace, "--ignore-not-found=true"],
context=context
)
return {"output": result.stdout, "stderr": result.stderr, "returncode": result.returncode}
@app.get("/logs")
def logs(pod: str, namespace: str = "default", context: str = None, tail: int = 100):
result = run_kubectl(
["logs", pod, "-n", namespace, "--tail", str(tail)],
context=context
)
return {"output": result.stdout, "stderr": result.stderr, "returncode": result.returncode}
@app.get("/exec")
def exec_pod(pod: str, namespace: str = "default", context: str = None, container: str = None, command: str = "ls"):
cmd = ["exec", pod, "-n", namespace]
if container:
cmd += ["-c", container]
cmd += ["--", "sh", "-c", command]
result = run_kubectl(cmd, context=context)
return {"output": result.stdout, "stderr": result.stderr, "returncode": result.returncode}
@app.get("/cordon")
def cordon(node: str, context: str = None):
result = run_kubectl(["cordon", node], context=context)
return {"output": result.stdout, "stderr": result.stderr, "returncode": result.returncode}
@app.get("/uncordon")
def uncordon(node: str, context: str = None):
result = run_kubectl(["uncordon", node], context=context)
return {"output": result.stdout, "stderr": result.stderr, "returncode": result.returncode}