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