diff --git a/monitor/views.py b/monitor/views.py index 0116e63..7187fd8 100644 --- a/monitor/views.py +++ b/monitor/views.py @@ -1,11 +1,13 @@ import json +import logging from django.http import JsonResponse from django.views.generic import TemplateView, View from django.utils.decorators import method_decorator from django.views.decorators.csrf import csrf_exempt -# Globaler In-Memory Speicher für die gepushten Cluster-States (simuliert eine DB) -# Struktur: { "repo_name": { ... d3_tree_data ... } } +logger = logging.getLogger(__name__) + +# Globaler In-Memory Speicher CLUSTER_STATES = {} class DashboardView(TemplateView): @@ -13,51 +15,61 @@ class DashboardView(TemplateView): @method_decorator(csrf_exempt, name='dispatch') class StatePushReceiverView(View): - """ - Der offizielle Enterprise-Endpunkt: Nimmt das anonymisierte 'terraform show -json' - aus JEDER Pipeline der Welt per POST entgegen. - """ def post(self, request, *args, **kwargs): - # Einfacher API-Token Check (Best Practice für dein Produkt) token = request.headers.get("Authorization") if token != "Bearer boday-cloudmon-super-token-2026": return JsonResponse({"error": "Unauthorized"}, status=401) try: - # Das rohe Terraform-JSON einlesen tf_data = json.loads(request.body.decode('utf-8')) - - # Wir holen uns den Repo-Namen aus den Query-Parametern (?repo=boday_tf) - repo_name = request.GET.get("repo", "default_cluster") + repo_name = request.GET.get("repo", "boday_tf") + + # KOMBI-PARSER: Erkennt automatisch, ob ein Plan-JSON oder ein State-JSON gepusht wurde + resources = [] + if "values" in tf_data and "root_module" in tf_data["values"]: + # Format aus 'terraform show -json' (Plan) + resources = tf_data["values"]["root_module"].get("resources", []) + elif "resources" in tf_data: + # Format aus dem rohen '.tfstate' (State) + resources = tf_data.get("resources", []) - # Parser für das standardisierte Terraform-JSON (Format von: terraform show -json) - # In der JSON-Ausgabe liegen alle angelegten Ressourcen flach im 'values' Block - values = tf_data.get("values", {}).get("root_module", {}).get("resources", []) - ingresses = [] services = {} deployments = {} - for res in values: + for res in resources: res_type = res.get("type") res_name = res.get("name") - values_inner = res.get("values", {}) + + # Attribut-Extraktion je nach JSON-Typ flexibel auflösen + instances = res.get("instances", []) + if isinstance(instances, list) and len(instances) > 0: + attributes = instances[0].get("attributes", {}) + else: + attributes = res.get("values", {}) - if res_type == "kubernetes_ingress_v1": + if not attributes: + continue + + # JSON-Struktur-Normalisierung für Kubernetes-Metadaten + metadata = attributes.get("metadata", [{}]) + actual_name = metadata[0].get("name", res_name) if isinstance(metadata, list) else metadata.get("name", res_name) + + if res_type == "kubernetes_ingress_v1" or res_type == "kubernetes_ingress": + spec = attributes.get("spec", [{}]) + rules = spec[0].get("rule", []) if isinstance(spec, list) else spec.get("rule", []) ingresses.append({ - "name": values_inner.get("metadata", [{}])[0].get("name", res_name) if isinstance(values_inner.get("metadata"), list) else values_inner.get("metadata", {}).get("name", res_name), - "rules": values_inner.get("spec", [{}])[0].get("rule", []) if isinstance(values_inner.get("spec"), list) else values_inner.get("spec", {}).get("rule", []) + "name": actual_name, + "rules": rules }) elif res_type in ["kubernetes_service", "kubernetes_service_v1"]: - svc_name = values_inner.get("metadata", [{}])[0].get("name", res_name) if isinstance(values_inner.get("metadata"), list) else values_inner.get("metadata", {}).get("name", res_name) - services[svc_name] = {"name": svc_name, "type": "Service"} + services[actual_name] = {"name": actual_name, "type": "Service"} elif res_type in ["kubernetes_deployment", "kubernetes_deployment_v1"]: - dep_name = values_inner.get("metadata", [{}])[0].get("name", res_name) if isinstance(values_inner.get("metadata"), list) else values_inner.get("metadata", {}).get("name", res_name) - deployments[dep_name] = {"name": dep_name, "type": "Deployment"} + deployments[actual_name] = {"name": actual_name, "type": "Deployment"} - # D3.js Baumstruktur aufbauen + # D3.js Baumhierarchie aufbauen root_node = { - "name": f"Cluster: {repo_name}", + "name": f"Repository: {repo_name}", "type": "Root", "children": [] } @@ -65,9 +77,12 @@ class StatePushReceiverView(View): for ing in ingresses: ing_node = {"name": ing["name"], "type": "Ingress", "children": []} for rule in ing["rules"]: - paths = rule.get("http", [{}])[0].get("path", []) if isinstance(rule.get("http"), list) else rule.get("http", {}).get("path", []) + http = rule.get("http", [{}]) + paths = http[0].get("path", []) if isinstance(http, list) else http.get("path", []) for path in 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") + backend = path.get("backend", [{}]) + service_block = backend[0].get("service", {}) if isinstance(backend, list) else backend.get("service", {}) + backend_svc = service_block.get("name") if backend_svc in services: svc_node = {"name": backend_svc, "type": "Service", "children": []} @@ -77,21 +92,19 @@ class StatePushReceiverView(View): ing_node["children"].append(svc_node) root_node["children"].append(ing_node) - # Im globalen RAM-Speicher sichern CLUSTER_STATES[repo_name] = root_node - return JsonResponse({"status": "success", "message": f"State für {repo_name} aktualisiert!"}) + return JsonResponse({"status": "success", "message": f"State für {repo_name} erfolgreich verarbeitet!"}) except Exception as e: + logger.error(f"Push-Verarbeitung fehlgeschlagen: {str(e)}") return JsonResponse({"error": str(e)}, status=400) - class ClusterGraphDataView(View): - """ Liefert die Daten an das D3.js Frontend aus """ def get(self, request, *args, **kwargs): - repo_name = request.GET.get("repo", "boday_tf") - # Holt die Daten aus dem Speicher, Fallback auf leeren Root-Knoten + # Holt das spezifische Repo aus dem Query-Parameter, Fallback auf boday_tf + repo_name = request.GET.get("repo", "bast/boday_tf") data = CLUSTER_STATES.get(repo_name, { - "name": f"Warte auf ersten Push für '{repo_name}'...", + "name": f"Warte auf Push für '{repo_name}'...", "type": "Root", "children": [] })