From 8a45a2275989f96ed29b2fab7dee462e0a724305 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Fri, 11 Jul 2025 14:43:26 +0200 Subject: [PATCH] sanitize kubernetes messages --- src/servala/core/models/service.py | 82 ++++++++++++++----- .../templates/includes/k8s_error.html | 14 ++++ .../frontend/templatetags/error_filters.py | 43 ++++++++++ 3 files changed, 117 insertions(+), 22 deletions(-) create mode 100644 src/servala/frontend/templates/includes/k8s_error.html create mode 100644 src/servala/frontend/templatetags/error_filters.py diff --git a/src/servala/core/models/service.py b/src/servala/core/models/service.py index b4d27b3..1bdb678 100644 --- a/src/servala/core/models/service.py +++ b/src/servala/core/models/service.py @@ -1,4 +1,5 @@ import copy +import html import json import re @@ -607,29 +608,60 @@ class ServiceInstance(ServalaModelMixin, models.Model): @classmethod def _format_kubernetes_error(cls, error_message): - """ - Format Kubernetes API error messages for better user experience. - Converts validation error arrays into unordered lists. - """ - # Pattern to match validation errors in brackets + if not error_message: + return {"message": "", "errors": None, "has_list": False} + + error_message = str(error_message).strip() + + # Pattern to match validation errors in brackets like [error1, error2, error3] pattern = r"\[([^\]]+)\]" match = re.search(pattern, error_message) if not match: - return error_message + return {"message": error_message, "errors": None, "has_list": False} - errors_text = match.group(1) - # Split by comma and clean up each error - errors = [error.strip() for error in errors_text.split(",")] + errors_text = match.group(1).strip() - if len(errors) > 1: - # Format as HTML unordered list - error_list = "".join(f"
  • {error}
  • " for error in errors) - # Replace the bracketed section with the formatted list - formatted_message = re.sub(pattern, f"", error_message) - return mark_safe(formatted_message) + if "," not in errors_text: + return {"message": error_message, "errors": None, "has_list": False} - return error_message + errors = [error.strip().strip("\"'") for error in errors_text.split(",")] + errors = [error for error in errors if error] + + if len(errors) <= 1: + return {"message": error_message, "errors": None, "has_list": False} + + base_message = re.sub(pattern, "", error_message).strip() + base_message = base_message.rstrip(":").strip() + + return {"message": base_message, "errors": errors, "has_list": True} + + @classmethod + def _safe_format_error(cls, error_data): + if not isinstance(error_data, dict): + return html.escape(str(error_data)) + + if not error_data.get("has_list", False): + return html.escape(error_data.get("message", "")) + + message = html.escape(error_data.get("message", "")) + errors = error_data.get("errors", []) + + if not errors: + return message + + escaped_errors = [html.escape(str(error)) for error in errors] + error_items = "".join(f"
  • {error}
  • " for error in escaped_errors) + + if message: + return mark_safe(f"{message}") + else: + return mark_safe(f"") + + @classmethod + def format_error_for_display(cls, error_message): + error_data = cls._format_kubernetes_error(error_message) + return cls._safe_format_error(error_data) @classmethod def create_instance(cls, name, organization, context, created_by, spec_data): @@ -684,18 +716,21 @@ class ServiceInstance(ServalaModelMixin, models.Model): try: error_body = json.loads(e.body) reason = error_body.get("message", str(e)) - formatted_reason = cls._format_kubernetes_error(reason) + error_data = cls._format_kubernetes_error(reason) + formatted_reason = cls._safe_format_error(error_data) message = _("Error reported by control plane: {reason}").format( reason=formatted_reason ) raise ValidationError(organization.add_support_message(message)) except (ValueError, TypeError): - formatted_error = cls._format_kubernetes_error(str(e)) + error_data = cls._format_kubernetes_error(str(e)) + formatted_error = cls._safe_format_error(error_data) message = _("Error reported by control plane: {error}").format( error=formatted_error ) raise ValidationError(organization.add_support_message(message)) - formatted_error = cls._format_kubernetes_error(str(e)) + error_data = cls._format_kubernetes_error(str(e)) + formatted_error = cls._safe_format_error(error_data) message = _("Error creating instance: {error}").format( error=formatted_error ) @@ -727,19 +762,22 @@ class ServiceInstance(ServalaModelMixin, models.Model): try: error_body = json.loads(e.body) reason = error_body.get("message", str(e)) - formatted_reason = self._format_kubernetes_error(reason) + error_data = self._format_kubernetes_error(reason) + formatted_reason = self._safe_format_error(error_data) message = _( "Error reported by control plane while updating instance: {reason}" ).format(reason=formatted_reason) raise ValidationError(self.organization.add_support_message(message)) except (ValueError, TypeError): - formatted_error = self._format_kubernetes_error(str(e)) + error_data = self._format_kubernetes_error(str(e)) + formatted_error = self._safe_format_error(error_data) message = _( "Error reported by control plane while updating instance: {error}" ).format(error=formatted_error) raise ValidationError(self.organization.add_support_message(message)) except Exception as e: - formatted_error = self._format_kubernetes_error(str(e)) + error_data = self._format_kubernetes_error(str(e)) + formatted_error = self._safe_format_error(error_data) message = _("Error updating instance: {error}").format( error=formatted_error ) diff --git a/src/servala/frontend/templates/includes/k8s_error.html b/src/servala/frontend/templates/includes/k8s_error.html new file mode 100644 index 0000000..f97474d --- /dev/null +++ b/src/servala/frontend/templates/includes/k8s_error.html @@ -0,0 +1,14 @@ +{% if show_error %} +
    + {% if has_list %} + {% if message %}{{ message }}{% endif %} + + {% else %} + {{ message }} + {% endif %} +
    +{% endif %} diff --git a/src/servala/frontend/templatetags/error_filters.py b/src/servala/frontend/templatetags/error_filters.py new file mode 100644 index 0000000..18c4c4d --- /dev/null +++ b/src/servala/frontend/templatetags/error_filters.py @@ -0,0 +1,43 @@ +""" +Template filters for safe error formatting. +""" + +import html +from django import template +from django.utils.safestring import mark_safe + +register = template.Library() + + +@register.filter +def format_k8s_error(error_data): + """ + Template filter to safely format Kubernetes error data. + Usage: {{ error_data|format_k8s_error }} + + Args: + error_data: Dictionary with structure from _format_kubernetes_error method + or a simple string + + Returns: + Safely formatted HTML string + """ + if not error_data: + return "" + + if not error_data.get("has_list", False): + return html.escape(error_data.get("message", "")) + + message = html.escape(error_data.get("message", "")) + errors = error_data.get("errors", []) + + if not errors: + return message + + escaped_errors = [html.escape(str(error)) for error in errors] + error_items = "".join(f"
  • {error}
  • " for error in escaped_errors) + + if message: + return mark_safe(f"{message}") + else: + return mark_safe(f"")