From 0eb07feeef8c367dc05e9cf9821e986d72c058ac Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Fri, 11 Apr 2025 09:14:58 +0200 Subject: [PATCH 1/9] 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 2/9] 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 3/9] 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 4/9] 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 5/9] 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 6/9] 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 7/9] 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 8/9] 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 9/9] 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