commit 5bb1e0f0d2c33050049e30f0cc9a2d2e641e9655 Author: Bast Date: Sun Nov 1 06:39:17 2026 -0600 first commit diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..d24fb38 --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/core/settings.py b/core/settings.py new file mode 100644 index 0000000..a1dcee0 --- /dev/null +++ b/core/settings.py @@ -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/' diff --git a/core/urls.py b/core/urls.py new file mode 100644 index 0000000..02f914d --- /dev/null +++ b/core/urls.py @@ -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'), +] diff --git a/core/wsgi.py b/core/wsgi.py new file mode 100644 index 0000000..d7e4f5c --- /dev/null +++ b/core/wsgi.py @@ -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() diff --git a/main.tf b/main.tf new file mode 100644 index 0000000..678f76d --- /dev/null +++ b/main.tf @@ -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 } + } + } + } + } + } + } +} + diff --git a/monitor/templates/monitor/dashboard.html b/monitor/templates/monitor/dashboard.html new file mode 100644 index 0000000..a83286d --- /dev/null +++ b/monitor/templates/monitor/dashboard.html @@ -0,0 +1,70 @@ + + + + + Boday-IT cloudmon Dashboard + + + + + + +
+
+
+

cloudmon // Cluster Map

+

Live Infrastructure Mindmap aus Terraform State-Daten

+
+ Live API Stream +
+ +
+ +
+
+ + + + diff --git a/monitor/views.py b/monitor/views.py new file mode 100644 index 0000000..8fc6b57 --- /dev/null +++ b/monitor/views.py @@ -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) + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..d323a27 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +Django==4.2.11 +requests==2.31.0 +gunicorn==21.2.0