first commit
This commit is contained in:
commit
5bb1e0f0d2
8 changed files with 309 additions and 0 deletions
15
Dockerfile
Normal file
15
Dockerfile
Normal 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
39
core/settings.py
Normal 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
7
core/urls.py
Normal 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
6
core/wsgi.py
Normal 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
85
main.tf
Normal 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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
70
monitor/templates/monitor/dashboard.html
Normal file
70
monitor/templates/monitor/dashboard.html
Normal 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
84
monitor/views.py
Normal 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
3
requirements.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
Django==4.2.11
|
||||||
|
requests==2.31.0
|
||||||
|
gunicorn==21.2.0
|
||||||
Loading…
Reference in a new issue