From 0eb07feeef8c367dc05e9cf9821e986d72c058ac Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Fri, 11 Apr 2025 09:14:58 +0200 Subject: [PATCH 01/17] Refactor access to group/version/kind --- src/servala/core/models/service.py | 54 +++++++++++++++++------------- 1 file changed, 31 insertions(+), 23 deletions(-) diff --git a/src/servala/core/models/service.py b/src/servala/core/models/service.py index 82d00da..d421f8b 100644 --- a/src/servala/core/models/service.py +++ b/src/servala/core/models/service.py @@ -355,11 +355,27 @@ class ControlPlaneCRD(ServalaModelMixin, models.Model): def __str__(self): return f"{self.service_offering} on {self.control_plane} with {self.service_definition}" + @cached_property + def group(self): + return self.service_definition.api_definition["group"] + + @cached_property + def version(self): + return self.service_definition.api_definition["version"] + + @cached_property + def kind(self): + return self.service_definition.api_definition["kind"] + + @cached_property + def kind_plural(self): + plural = self.kind.lower() + if not plural.endswith("s"): + plural = f"{plural}s" + return plural + @cached_property def resource_definition(self): - kind = self.service_definition.api_definition["kind"] - group = self.service_definition.api_definition["group"] - version = self.service_definition.api_definition["version"] client = self.control_plane.get_kubernetes_client() extensions_api = kubernetes.client.ApiextensionsV1Api(client) @@ -368,10 +384,10 @@ class ControlPlaneCRD(ServalaModelMixin, models.Model): for crd in crds.items: if matching_crd: break - if crd.spec.group == group: + if crd.spec.group == self.group: for served_version in crd.spec.versions: - if served_version.name == version and served_version.served: - if crd.spec.names.kind == kind: + if served_version.name == self.version and served_version.served: + if crd.spec.names.kind == self.kind: matching_crd = crd break return matching_crd @@ -382,9 +398,8 @@ class ControlPlaneCRD(ServalaModelMixin, models.Model): if result := cache.get(cache_key): return result - version = self.service_definition.api_definition["version"] for v in self.resource_definition.spec.versions: - if v.name == version: + if v.name == self.version: result = v.schema.open_apiv3_schema.to_dict() timeout_seconds = 60 * 60 * 24 cache.set(cache_key, result, timeout=timeout_seconds) @@ -395,9 +410,9 @@ class ControlPlaneCRD(ServalaModelMixin, models.Model): from servala.core.crd import generate_django_model kwargs = { - key: value - for key, value in self.service_definition.api_definition.items() - if key in ("group", "version", "kind") + "group": self.group, + "version": self.version, + "kind": self.kind, } return generate_django_model(self.resource_schema, **kwargs) @@ -517,12 +532,9 @@ class ServiceInstance(ServalaModelMixin, models.Model): ) try: - group = context.service_definition.api_definition["group"] - version = context.service_definition.api_definition["version"] - kind = context.service_definition.api_definition["kind"] create_data = { - "apiVersion": f"{group}/{version}", - "kind": kind, + "apiVersion": f"{context.group}/{context.version}", + "kind": context.kind, "metadata": { "name": name, "namespace": organization.namespace, @@ -534,15 +546,11 @@ class ServiceInstance(ServalaModelMixin, models.Model): api_instance = client.CustomObjectsApi( context.control_plane.get_kubernetes_client() ) - plural = kind.lower() - if not plural.endswith("s"): - plural = f"{plural}s" - api_instance.create_namespaced_custom_object( - group=group, - version=version, + group=context.group, + version=context.version, namespace=organization.namespace, - plural=plural, + plural=context.kind_plural, body=create_data, ) except Exception as e: From c4522e31e873ea3f548a6fa5085876bfde054251 Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Fri, 11 Apr 2025 09:53:47 +0200 Subject: [PATCH 02/17] Implement k8s instance retrieval --- src/servala/core/models/service.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/servala/core/models/service.py b/src/servala/core/models/service.py index d421f8b..7280eaa 100644 --- a/src/servala/core/models/service.py +++ b/src/servala/core/models/service.py @@ -564,3 +564,23 @@ class ServiceInstance(ServalaModelMixin, models.Model): raise ValidationError(_("Kubernetes API error: {}").format(str(e))) raise ValidationError(_("Error creating instance: {}").format(str(e))) return instance + + @cached_property + def kubernetes_object(self): + """Fetch the Kubernetes custom resource object""" + try: + api_instance = client.CustomObjectsApi( + self.context.control_plane.get_kubernetes_client() + ) + + return api_instance.get_namespaced_custom_object( + group=self.context.group, + version=self.context.version, + namespace=self.organization.namespace, + plural=self.context.kind_plural, + name=self.name, + ) + except ApiException as e: + if e.status == 404: + return None + raise From 912842bd8275a7018a59e73cb1ec0fbbc78887eb Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Fri, 11 Apr 2025 10:10:41 +0200 Subject: [PATCH 03/17] Parse out spec data --- src/servala/core/models/service.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/servala/core/models/service.py b/src/servala/core/models/service.py index 7280eaa..3ba781e 100644 --- a/src/servala/core/models/service.py +++ b/src/servala/core/models/service.py @@ -584,3 +584,16 @@ class ServiceInstance(ServalaModelMixin, models.Model): if e.status == 404: return None raise + + @cached_property + def spec(self): + if not self.kubernetes_object: + return {} + if not (spec := self.kubernetes_object.get("spec")): + return {} + + # Remove fields that shouldn't be displayed + spec = spec.copy() + spec.pop("resourceRef", None) + spec.pop("writeConnectionSecretToRef", None) + return spec From 6d34e3abdc730697940f98bf06ea152c2df049b5 Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Fri, 11 Apr 2025 12:47:31 +0200 Subject: [PATCH 04/17] Parse status conditions --- src/servala/core/models/service.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/servala/core/models/service.py b/src/servala/core/models/service.py index 3ba781e..835689f 100644 --- a/src/servala/core/models/service.py +++ b/src/servala/core/models/service.py @@ -597,3 +597,11 @@ class ServiceInstance(ServalaModelMixin, models.Model): spec.pop("resourceRef", None) spec.pop("writeConnectionSecretToRef", None) return spec + + @cached_property + def status_conditions(self): + if not self.kubernetes_object: + return [] + if not (status := self.kubernetes_object.get("status")): + return [] + return status.get("conditions") or [] From 93916cdcbcca28b0c5f8fe5a50fc65cd05b54580 Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Fri, 11 Apr 2025 13:34:10 +0200 Subject: [PATCH 05/17] Show conditions in detail view --- .../service_instance_detail.html | 53 +++++++++++++++++-- 1 file changed, 49 insertions(+), 4 deletions(-) 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 7a949f1..b85642f 100644 --- a/src/servala/frontend/templates/frontend/organizations/service_instance_detail.html +++ b/src/servala/frontend/templates/frontend/organizations/service_instance_detail.html @@ -7,10 +7,10 @@ {% endblock html_title %} {% block content %}
-
-
-
-
+
+
+
+
{% translate "Details" %}
{% translate "Service" %}
@@ -49,6 +49,51 @@
+ {% if instance.status_conditions %} +
+
+
+
+
+
{% translate "Status Conditions" %}
+
+ + + + + + + + + + + + {% for condition in instance.status_conditions %} + + + + + + + + {% endfor %} + +
{% translate "Type" %}{% translate "Status" %}{% translate "Last Transition Time" %}{% translate "Reason" %}{% translate "Message" %}
{{ condition.type }} + {% if condition.status == "True" %} + True + {% elif condition.status == "False" %} + False + {% else %} + {{ condition.status }} + {% endif %} + {{ condition.lastTransitionTime }}{{ condition.reason }}{{ condition.message }}
+
+
+
+
+
+
+ {% endif %}
{% endblock content %} From 7afc4400b7bc22c39f86b94799afa9b40b58e51e Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Fri, 11 Apr 2025 14:00:38 +0200 Subject: [PATCH 06/17] Improve status condition display --- .../service_instance_detail.html | 70 ++++++++++--------- 1 file changed, 36 insertions(+), 34 deletions(-) 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 b85642f..7ae410f 100644 --- a/src/servala/frontend/templates/frontend/organizations/service_instance_detail.html +++ b/src/servala/frontend/templates/frontend/organizations/service_instance_detail.html @@ -10,8 +10,10 @@
+
+

{% translate "Details" %}

+
-
{% translate "Details" %}
{% translate "Service" %}
@@ -52,42 +54,42 @@ {% if instance.status_conditions %}
+
+

{% translate "Status" %}

+
-
-
-
{% translate "Status Conditions" %}
-
- - +
+
+
+ + + + + + + + + + + {% for condition in instance.status_conditions %} - - - - - + + + + + - - - {% for condition in instance.status_conditions %} - - - - - - - - {% endfor %} - -
{% translate "Type" %}{% translate "Status" %}{% translate "Last Transition Time" %}{% translate "Reason" %}{% translate "Message" %}
{% translate "Type" %}{% translate "Status" %}{% translate "Last Transition Time" %}{% translate "Reason" %}{% translate "Message" %}{{ condition.type }} + {% if condition.status == "True" %} + True + {% elif condition.status == "False" %} + False + {% else %} + {{ condition.status }} + {% endif %} + {{ condition.lastTransitionTime }}{{ condition.reason }}{{ condition.message }}
{{ condition.type }} - {% if condition.status == "True" %} - True - {% elif condition.status == "False" %} - False - {% else %} - {{ condition.status }} - {% endif %} - {{ condition.lastTransitionTime }}{{ condition.reason }}{{ condition.message }}
-
+ {% endfor %} + +
From 40811cbc082d3f35746c500d0246c28234737789 Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Fri, 11 Apr 2025 16:40:32 +0200 Subject: [PATCH 07/17] Very rough spec display in instance detail --- .../service_instance_detail.html | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) 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 7ae410f..3787e3c 100644 --- a/src/servala/frontend/templates/frontend/organizations/service_instance_detail.html +++ b/src/servala/frontend/templates/frontend/organizations/service_instance_detail.html @@ -96,6 +96,41 @@
{% endif %} + {% if instance.spec %} +
+
+
+

{% translate "Specification" %}

+
+
+
+ + + + + + + + + {% for key, value in instance.spec.items %} + + + + + {% endfor %} + +
{% translate "Property" %}{% translate "Value" %}
{{ key }} + {% if value|default:""|stringformat:"s"|slice:":1" == "{" or value|default:""|stringformat:"s"|slice:":1" == "[" %} +
{{ value|pprint }}
+ {% else %} + {{ value }} + {% endif %} +
+
+
+
+
+ {% endif %}
{% endblock content %} From 60b47ed6c858c271f8903d88d474028142e6beb9 Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Fri, 11 Apr 2025 16:55:33 +0200 Subject: [PATCH 08/17] WIP: connection credentials --- src/servala/core/models/service.py | 77 +++++++++++++++++++ .../service_instance_detail.html | 33 ++++++++ 2 files changed, 110 insertions(+) diff --git a/src/servala/core/models/service.py b/src/servala/core/models/service.py index 835689f..550a489 100644 --- a/src/servala/core/models/service.py +++ b/src/servala/core/models/service.py @@ -605,3 +605,80 @@ class ServiceInstance(ServalaModelMixin, models.Model): if not (status := self.kubernetes_object.get("status")): return [] return status.get("conditions") or [] + + @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. + """ + if not self.kubernetes_object: + return {} + if not ( + resource_ref := self.kubernetes_object.get("spec", {}).get("resourceRef") + ): + 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 + ) + + # Secret data is base64 encoded + credentials = {} + if hasattr(secret, "data") and secret.data: + import base64 + + for key, value in secret.data.items(): + try: + credentials[key] = base64.b64decode(value).decode("utf-8") + except Exception: + credentials[key] = f"" + + return credentials + except ApiException as e: + return {"error": str(e)} + except Exception as e: + return {"error": str(e)} 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 3787e3c..b4049a8 100644 --- a/src/servala/frontend/templates/frontend/organizations/service_instance_detail.html +++ b/src/servala/frontend/templates/frontend/organizations/service_instance_detail.html @@ -131,6 +131,39 @@
{% endif %} + {% if instance.connection_credentials %} +
+
+

{% translate "Connection Credentials" %}

+
+
+
+ + + + + + + + + {% for key, value in instance.connection_credentials.items %} + + + + + {% endfor %} + +
{% translate "Name" %}{% translate "Value" %}
{{ key }} + {% if key == "error" %} + {{ value }} + {% else %} + {{ value }} + {% endif %} +
+
+
+
+ {% endif %}
{% endblock content %} From 2a359b50ef9ad8e31294b8814ea3991ce3e17071 Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Fri, 11 Apr 2025 17:41:58 +0200 Subject: [PATCH 09/17] Add debugging template filter --- src/servala/frontend/templatetags/pprint_filters.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 src/servala/frontend/templatetags/pprint_filters.py diff --git a/src/servala/frontend/templatetags/pprint_filters.py b/src/servala/frontend/templatetags/pprint_filters.py new file mode 100644 index 0000000..e20772e --- /dev/null +++ b/src/servala/frontend/templatetags/pprint_filters.py @@ -0,0 +1,12 @@ +import json + +from django import template + +register = template.Library() + + +@register.filter +def pprint(value): + if isinstance(value, (dict, list)): + return json.dumps(value, indent=2) + return value From c8eaa99d381bdff2e5941d975b98ca7f1c1ca762 Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Mon, 14 Apr 2025 10:50:16 +0200 Subject: [PATCH 10/17] Improve card display --- .../frontend/organizations/service_instance_detail.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 b4049a8..f83fb84 100644 --- a/src/servala/frontend/templates/frontend/organizations/service_instance_detail.html +++ b/src/servala/frontend/templates/frontend/organizations/service_instance_detail.html @@ -8,7 +8,7 @@ {% block content %}
-
+

{% translate "Details" %}

@@ -52,7 +52,7 @@
{% if instance.status_conditions %} -
+

{% translate "Status" %}

From 6160f48d6160107dfa81adf30601a8068ff46a19 Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Thu, 17 Apr 2025 10:20:22 +0200 Subject: [PATCH 11/17] 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 12/17] 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" From d2ed55b6067f9c9336ad4218479d8bf35b21c098 Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Thu, 17 Apr 2025 15:41:01 +0200 Subject: [PATCH 13/17] Fix secret retrieval --- src/servala/core/models/service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/servala/core/models/service.py b/src/servala/core/models/service.py index 2f83d8d..0a59989 100644 --- a/src/servala/core/models/service.py +++ b/src/servala/core/models/service.py @@ -636,7 +636,7 @@ class ServiceInstance(ServalaModelMixin, models.Model): self.context.control_plane.get_kubernetes_client() ) secret = v1.read_namespaced_secret( - name=secret_name, namespace=secret_ref.get("namespace") + name=secret_name, namespace=self.organization.namespace ) # Secret data is base64 encoded From 9ddca7c0a4d5cdd886d2666cf66d7a7bbac3689d Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Thu, 17 Apr 2025 15:42:10 +0200 Subject: [PATCH 14/17] Fix secret retrieval condition --- src/servala/core/models/service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/servala/core/models/service.py b/src/servala/core/models/service.py index 0a59989..20a843c 100644 --- a/src/servala/core/models/service.py +++ b/src/servala/core/models/service.py @@ -618,7 +618,7 @@ class ServiceInstance(ServalaModelMixin, models.Model): # Check if secrets are available based on conditions secrets_available = any( [ - condition.get("type") == "Status" and condition.get("status") == "True" + condition.get("type") == "Ready" and condition.get("status") == "True" for condition in self.status_conditions ] ) From aa73805cf602cab339fd448d9fbe9c04999364d1 Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Thu, 17 Apr 2025 15:50:28 +0200 Subject: [PATCH 15/17] Use full spec data to retrieve secret ref --- src/servala/core/models/service.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/servala/core/models/service.py b/src/servala/core/models/service.py index 20a843c..baca840 100644 --- a/src/servala/core/models/service.py +++ b/src/servala/core/models/service.py @@ -625,7 +625,8 @@ class ServiceInstance(ServalaModelMixin, models.Model): if not secrets_available: return {} - if not (secret_ref := self.spec.get("writeConnectionSecretToRef")): + spec = self.kubernetes_object.get("spec") + if not (secret_ref := spec.get("writeConnectionSecretToRef")): return {} if not (secret_name := secret_ref.get("name")): return {} From a5875cf2b9e8c27cf367ee9e9710a05d406db536 Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Thu, 17 Apr 2025 16:58:37 +0200 Subject: [PATCH 16/17] Fix ControlPlaneAdmin ordering warning --- src/servala/core/admin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/servala/core/admin.py b/src/servala/core/admin.py index 746d5da..7ff602b 100644 --- a/src/servala/core/admin.py +++ b/src/servala/core/admin.py @@ -126,6 +126,7 @@ class ControlPlaneAdmin(admin.ModelAdmin): search_fields = ("name", "description") autocomplete_fields = ("cloud_provider",) actions = ["test_kubernetes_connection"] + ordering = ('name',) fieldsets = ( ( From ff5761ddc7d5e506fa4ec6a62c42ef2f89ceb355 Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Thu, 17 Apr 2025 17:03:51 +0200 Subject: [PATCH 17/17] Code style --- src/servala/core/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/servala/core/admin.py b/src/servala/core/admin.py index 7ff602b..740b7bf 100644 --- a/src/servala/core/admin.py +++ b/src/servala/core/admin.py @@ -126,7 +126,7 @@ class ControlPlaneAdmin(admin.ModelAdmin): search_fields = ("name", "description") autocomplete_fields = ("cloud_provider",) actions = ["test_kubernetes_connection"] - ordering = ('name',) + ordering = ("name",) fieldsets = ( (