From 6160f48d6160107dfa81adf30601a8068ff46a19 Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Thu, 17 Apr 2025 10:20:22 +0200 Subject: [PATCH 1/2] Possibly fix secret retrieval (untested) --- src/servala/core/models/service.py | 61 +++++++++--------------------- 1 file changed, 17 insertions(+), 44 deletions(-) diff --git a/src/servala/core/models/service.py b/src/servala/core/models/service.py index 550a489..2f83d8d 100644 --- a/src/servala/core/models/service.py +++ b/src/servala/core/models/service.py @@ -609,61 +609,34 @@ class ServiceInstance(ServalaModelMixin, models.Model): @cached_property def connection_credentials(self): """ - Get connection credentials via spec.resourceRef. - The resource referenced there has the information which secret - we want in spec.writeConnectionSecretToRef.name and spec.writeConnectionSecretToRef.namespace. + Get connection credentials directly from the resource's writeConnectionSecretToRef + after checking that secret conditions are available. """ if not self.kubernetes_object: return {} - if not ( - resource_ref := self.kubernetes_object.get("spec", {}).get("resourceRef") - ): + + # Check if secrets are available based on conditions + secrets_available = any( + [ + condition.get("type") == "Status" and condition.get("status") == "True" + for condition in self.status_conditions + ] + ) + if not secrets_available: + return {} + + if not (secret_ref := self.spec.get("writeConnectionSecretToRef")): + return {} + if not (secret_name := secret_ref.get("name")): return {} try: - group = resource_ref.get("apiVersion", "").split("/")[0] - version = resource_ref.get("apiVersion", "").split("/")[1] - kind = resource_ref.get("kind") - name = resource_ref.get("name") - namespace = resource_ref.get("namespace", self.organization.namespace) - - if not all([group, version, kind, name]): - return {} - - plural = kind.lower() - if not plural.endswith("s"): - plural = f"{plural}s" - - api_instance = client.CustomObjectsApi( - self.context.control_plane.get_kubernetes_client() - ) - - referenced_obj = api_instance.get_namespaced_custom_object( - group=group, - version=version, - namespace=namespace, - plural=plural, - name=name, - ) - - secret_ref = referenced_obj.get("spec", {}).get( - "writeConnectionSecretToRef" - ) - if not secret_ref: - return {} - - secret_name = secret_ref.get("name") - secret_namespace = secret_ref.get("namespace", namespace) - - if not secret_name: - return {} - # Get the secret data v1 = kubernetes.client.CoreV1Api( self.context.control_plane.get_kubernetes_client() ) secret = v1.read_namespaced_secret( - name=secret_name, namespace=secret_namespace + name=secret_name, namespace=secret_ref.get("namespace") ) # Secret data is base64 encoded From b2d900435974d2de91c7d4c5440e2312d627a6e5 Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Thu, 17 Apr 2025 10:47:08 +0200 Subject: [PATCH 2/2] Display spec data tabbed like in form --- pyproject.toml | 2 +- .../service_instance_detail.html | 73 ++++++++---- src/servala/frontend/views/service.py | 111 ++++++++++++++++++ 3 files changed, 163 insertions(+), 23 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 27d42c7..3cded0f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,7 +43,7 @@ known_first_party = "servala" [tool.flake8] max-line-length = 160 exclude = ".venv" -ignore = "E203" +ignore = "E203,W503" [tool.djlint] extend_exclude = "src/servala/static/mazer" diff --git a/src/servala/frontend/templates/frontend/organizations/service_instance_detail.html b/src/servala/frontend/templates/frontend/organizations/service_instance_detail.html index f83fb84..b9cf4dc 100644 --- a/src/servala/frontend/templates/frontend/organizations/service_instance_detail.html +++ b/src/servala/frontend/templates/frontend/organizations/service_instance_detail.html @@ -103,29 +103,58 @@

{% translate "Specification" %}

-
- - - - - - - - - {% for key, value in instance.spec.items %} - - - - +
+
+ +
-
{% translate "Property" %}{% translate "Value" %}
{{ key }} - {% if value|default:""|stringformat:"s"|slice:":1" == "{" or value|default:""|stringformat:"s"|slice:":1" == "[" %} -
{{ value|pprint }}
- {% else %} - {{ value }} - {% endif %} -
+ + +
+ {% for fieldset in spec_fieldsets %} +
+ +
+ {% for field in fieldset.fields %} +
{{ field.label }}
+
+ {% if field.value|default:""|stringformat:"s"|slice:":1" == "{" or field.value|default:""|stringformat:"s"|slice:":1" == "[" %} +
{{ field.value|pprint }}
+ {% else %} + {{ field.value }} + {% endif %} +
+ {% endfor %} +
+ + {% for sub_key, sub_fieldset in fieldset.fieldsets.items %} +
{{ sub_fieldset.title }}
+
+ {% for field in sub_fieldset.fields %} +
{{ field.label }}
+
+ {% if field.value|default:""|stringformat:"s"|slice:":1" == "{" or field.value|default:""|stringformat:"s"|slice:":1" == "[" %} +
{{ field.value|pprint }}
+ {% else %} + {{ field.value }} + {% endif %} +
+ {% endfor %} +
+ {% endfor %} +
+ {% endfor %} +
+
diff --git a/src/servala/frontend/views/service.py b/src/servala/frontend/views/service.py index 6ee6b1e..b6ecc42 100644 --- a/src/servala/frontend/views/service.py +++ b/src/servala/frontend/views/service.py @@ -4,6 +4,7 @@ from django.shortcuts import redirect from django.utils.functional import cached_property from django.views.generic import DetailView, ListView +from servala.core.crd import deslugify from servala.core.models import ( ControlPlaneCRD, Service, @@ -168,6 +169,116 @@ class ServiceInstanceDetailView(OrganizationViewMixin, DetailView): "context__service_definition__service", ) + def get_nested_spec(self): + """ + Organize spec data into fieldsets similar to how the form does it. + """ + spec = self.object.spec or {} + + # Process spec fields + others = [] + nested_fieldsets = {} + + # First pass: organize fields into nested structures + for key, value in spec.items(): + if isinstance(value, dict): + # This is a nested structure + if key not in nested_fieldsets: + nested_fieldsets[key] = { + "title": deslugify(key), + "fields": [], + "fieldsets": {}, + } + + # Process fields in the nested structure + for sub_key, sub_value in value.items(): + if isinstance(sub_value, dict): + # Even deeper nesting + if sub_key not in nested_fieldsets[key]["fieldsets"]: + nested_fieldsets[key]["fieldsets"][sub_key] = { + "title": deslugify(sub_key), + "fields": [], + } + + # Add fields from the deeper level + for leaf_key, leaf_value in sub_value.items(): + nested_fieldsets[key]["fieldsets"][sub_key][ + "fields" + ].append( + { + "key": leaf_key, + "label": deslugify(leaf_key), + "value": leaf_value, + } + ) + else: + # Add field to parent level + nested_fieldsets[key]["fields"].append( + { + "key": sub_key, + "label": deslugify(sub_key), + "value": sub_value, + } + ) + else: + # This is a top-level field + others.append( + { + "key": key, + "label": deslugify(key), + "value": value, + } + ) + + # Second pass: Promote fields based on count + for group_key, group in list(nested_fieldsets.items()): + # Promote single sub-fieldsets to parent + for sub_key, sub_fieldset in list(group["fieldsets"].items()): + if len(sub_fieldset["fields"]) == 1: + field = sub_fieldset["fields"][0] + field["label"] = f"{sub_fieldset['title']}: {field['label']}" + group["fields"].append(field) + del group["fieldsets"][sub_key] + + # Move single-field groups to others + total_fields = len(group["fields"]) + for sub_fieldset in group["fieldsets"].values(): + total_fields += len(sub_fieldset["fields"]) + + if ( + total_fields == 1 + and len(group["fields"]) == 1 + and not group["fieldsets"] + ): + field = group["fields"][0] + field["label"] = f"{group['title']}: {field['label']}" + others.append(field) + del nested_fieldsets[group_key] + elif total_fields == 0: + del nested_fieldsets[group_key] + + fieldsets = [] + if others: + fieldsets.append( + { + "title": "General", + "fields": others, + "fieldsets": {}, + } + ) + # Create fieldsets from the organized data + for group in nested_fieldsets.values(): + fieldsets.append(group) + + return fieldsets + + def get_context_data(self, **kwargs): + """Return service instance for the current organization.""" + context = super().get_context_data(**kwargs) + if self.object.spec: + context["spec_fieldsets"] = self.get_nested_spec() + return context + class ServiceInstanceListView(OrganizationViewMixin, ListView): template_name = "frontend/organizations/service_instances.html"