first commit

This commit is contained in:
Bast 2026-11-01 06:39:17 -06:00
commit 5bb1e0f0d2
8 changed files with 309 additions and 0 deletions

15
Dockerfile Normal file
View file

@ -0,0 +1,15 @@
FROM python:3.10-slim
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
WORKDIR /app
COPY requirements.txt /app/
RUN pip install --no-cache-dir --upgrade pip && pip install --no-cache-dir -r requirements.txt
COPY . /app/
EXPOSE 8000
CMD ["gunicorn", "core.wsgi:application", "--bind", "0.0.0.0:8000"]

39
core/settings.py Normal file
View file

@ -0,0 +1,39 @@
import os
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent
SECRET_KEY = 'django-insecure-cloudmon-super-secret-key-dashboard'
DEBUG = True
ALLOWED_HOSTS = ['*'] # Für den Sandbox-Betrieb im Cluster absolut legitim
INSTALLED_APPS = [
'django.contrib.staticfiles',
'monitor',
]
MIDDLEWARE = [
'django.middleware.common.CommonMiddleware',
]
ROOT_URLCONF = 'core.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
],
},
},
]
WSGI_APPLICATION = 'core.wsgi.application'
DATABASES = {} # REINER IN-MEMORY PARSER: Wir brauchen keine lokale Datenbank!
STATIC_URL = 'static/'

7
core/urls.py Normal file
View file

@ -0,0 +1,7 @@
from django.urls import path
from monitor.views import DashboardView, ClusterGraphDataView
urlpatterns = [
path('', DashboardView.as_view(), name='dashboard'),
path('api/cluster-data/', ClusterGraphDataView.as_view(), name='cluster_data'),
]

6
core/wsgi.py Normal file
View file

@ -0,0 +1,6 @@
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('臨界_SETTINGS_MODULE', 'core.settings')
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings')
application = get_wsgi_application()

85
main.tf Normal file
View file

@ -0,0 +1,85 @@
terraform {
required_providers {
kubernetes = {
source = "hashicorp/kubernetes"
version = "~> 2.26"
}
}
}
variable "app_version" {
type = string
default = "latest"
}
provider "kubernetes" {
config_path = "./kind_kubeconfig"
host = "https://local-k3s-platform:6443"
insecure = true
}
# 1. DAS DASHBOARD DEPLOYMENT
resource "kubernetes_deployment_v1" "cloudmon" {
metadata {
name = "cloudmon"
namespace = "default"
labels = { app = "cloudmon" }
}
spec {
replicas = 1
selector { match_labels = { app = "cloudmon" } }
template {
metadata { labels = { app = "cloudmon" } }
spec {
container {
name = "dashboard"
image = "registry.local/bast/cloudmon:${var.app_version}"
image_pull_policy = "Always"
port { container_port = 8000 }
}
}
}
}
}
# 2. DER KUBERNETES SERVICE (Port 8000 intern freischalten)
resource "kubernetes_service_v1" "cloudmon_service" {
metadata {
name = "cloudmon-service"
namespace = "default"
}
spec {
selector = { app = "cloudmon" }
port {
port = 80
target_port = 8000
}
}
}
# 3. DAS INGRESS ROUTING (Macht cloudmon unter cloudmon.local über Port 8080 erreichbar!)
resource "kubernetes_ingress_v1" "cloudmon_ingress" {
metadata {
name = "cloudmon-ingress"
namespace = "default"
annotations = { "kubernetes.io/ingress.class" = "nginx" }
}
spec {
rule {
host = "cloudmon.local"
http {
path {
path = "/"
path_type = "Prefix"
backend {
service {
name = kubernetes_service_v1.cloudmon_service.metadata[0].name
port { number = 80 }
}
}
}
}
}
}
}

View file

@ -0,0 +1,70 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Boday-IT cloudmon Dashboard</title>
<script src="https://d3js.org"></script>
<script src="https://tailwindcss.com"></script>
<style>
.node circle { fill: #fff; stroke: #4f46e5; stroke-width: 3px; }
.node text { font: 12px sans-serif; font-weight: 600; fill: #1f2937; }
.link { fill: none; stroke: #cbd5e1; stroke-width: 2px; }
</style>
</head>
<body class="bg-slate-50 min-h-screen">
<div class="p-6 max-w-7xl mx-auto">
<header class="mb-8 flex justify-between items-center">
<div>
<h1 class="text-2xl font-bold text-slate-800">cloudmon // Cluster Map</h1>
<p class="text-sm text-slate-500">Live Infrastructure Mindmap aus Terraform State-Daten</p>
</div>
<span class="px-3 py-1 bg-emerald-100 text-emerald-800 rounded-full text-xs font-semibold animate-pulse">Live API Stream</span>
</header>
<div class="bg-white rounded-xl shadow-md p-6 overflow-x-auto border border-slate-200 flex justify-center">
<svg id="mindmap" width="1000" height="500"></svg>
</div>
</div>
<script>
d3.json("/api/cluster-data/").then(function(data) {
const svg = d3.select("#mindmap"),
width = +svg.attr("width"),
height = +svg.attr("height");
const g = svg.append("g").attr("transform", "translate(150,0)");
const tree = d3.tree().size([height - 50, width - 350]);
const root = d3.hierarchy(data, d => d.children);
tree(root);
g.selectAll(".link")
.data(root.links())
.enter().append("path")
.attr("class", "link")
.attr("d", d3.linkHorizontal().x(d => d.y).y(d => d.x));
const node = g.selectAll(".node")
.data(root.descendants())
.enter().append("g")
.attr("class", "node")
.attr("transform", d => `translate(${d.y},${d.x})`);
node.append("circle")
.attr("r", 8)
.style("stroke", d => {
if (d.data.type === "Root") return "#10b981";
if (d.data.type === "Ingress") return "#3b82f6";
if (d.data.type === "Service") return "#f59e0b";
return "#6366f1";
});
node.append("text")
.attr("dy", ".35em")
.attr("x", d => d.children ? -13 : 13)
.style("text-anchor", d => d.children ? "end" : "start")
.text(d => `[${d.data.type}] ${d.data.name}`);
});
</script>
</body>
</html>

84
monitor/views.py Normal file
View file

@ -0,0 +1,84 @@
import requests
from django.http import JsonResponse
from django.views.generic import TemplateView, View
class DashboardView(TemplateView):
template_name = "monitor/dashboard.html"
class ClusterGraphDataView(View):
def get(self, request, *args, **kwargs):
# Holt den State direkt aus deinem Forgejo-HTTP-Backend
forgejo_url = "http://forgejo:3000/api/v1/repos/bast/boday_tf/terraform/state/sandbox"
headers = {"Authorization": "token z6xlwg3dSRYZ59zUxfrztDJasG6VkvulFyTwtJQS"}
try:
response = requests.get(forgejo_url, headers=headers, timeout=5)
if response.status_code != 200:
return JsonResponse({"error": "Terraform State konnte nicht geladen werden"}, status=response.status_code)
state_json = response.json()
resources = state_json.get("resources", [])
ingresses = []
services = {}
deployments = {}
# Flache Terraform-Struktur filtern
for res in resources:
res_type = res.get("type")
res_name = res.get("name")
instances = res.get("instances", [])
if not instances: continue
# act/gitea liefert instanzen als Liste
if isinstance(instances, list) and len(instances) > 0:
attributes = instances[0].get("attributes", {})
else:
attributes = instances.get("attributes", {})
if res_type == "kubernetes_ingress_v1":
ingresses.append({
"name": attributes.get("metadata", [{}])[0].get("name", res_name) if isinstance(attributes.get("metadata"), list) else attributes.get("metadata", {}).get("name", res_name),
"rules": attributes.get("spec", [{}])[0].get("rule", []) if isinstance(attributes.get("spec"), list) else attributes.get("spec", {}).get("rule", [])
})
elif res_type == "kubernetes_service" or res_type == "kubernetes_service_v1":
svc_actual_name = attributes.get("metadata", [{}])[0].get("name", res_name) if isinstance(attributes.get("metadata"), list) else attributes.get("metadata", {}).get("name", res_name)
services[svc_actual_name] = {"name": svc_actual_name, "type": "Service"}
elif res_type == "kubernetes_deployment" or res_type == "kubernetes_deployment_v1":
dep_actual_name = attributes.get("metadata", [{}])[0].get("name", res_name) if isinstance(attributes.get("metadata"), list) else attributes.get("metadata", {}).get("name", res_name)
deployments[dep_actual_name] = {"name": dep_actual_name, "type": "Deployment"}
# D3.js Baumhierarchie aufbauen
root_node = {
"name": "Forgejo Control Plane",
"type": "Root",
"children": []
}
for ing in ingresses:
ing_node = {"name": ing["name"], "type": "Ingress", "children": []}
for rule in ing["rules"]:
http_paths = rule.get("http", [{}])[0].get("path", []) if isinstance(rule.get("http"), list) else rule.get("http", {}).get("path", [])
for path in http_paths:
backend_svc = path.get("backend", [{}])[0].get("service", [{}])[0].get("name") if isinstance(path.get("backend"), list) else path.get("backend", {}).get("service", {}).get("name")
if backend_svc in services:
svc_node = {"name": backend_svc, "type": "Service", "children": []}
for dep_name in deployments.keys():
if dep_name in backend_svc or backend_svc in dep_name:
svc_node["children"].append({
"name": dep_name,
"type": "Deployment",
"children": []
})
ing_node["children"].append(svc_node)
root_node["children"].append(ing_node)
return JsonResponse(root_node)
except Exception as e:
return JsonResponse({"error": str(e)}, status=500)

3
requirements.txt Normal file
View file

@ -0,0 +1,3 @@
Django==4.2.11
requests==2.31.0
gunicorn==21.2.0