diff --git a/pyproject.toml b/pyproject.toml index 51ae0c0..9f7812f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,7 +44,7 @@ known_first_party = "servala" [tool.flake8] max-line-length = 160 exclude = ".venv" -ignore = "E203,W503" +ignore = "E203" [tool.djlint] extend_exclude = "src/servala/static/mazer" diff --git a/src/servala/core/admin.py b/src/servala/core/admin.py index 740b7bf..746d5da 100644 --- a/src/servala/core/admin.py +++ b/src/servala/core/admin.py @@ -126,7 +126,6 @@ class ControlPlaneAdmin(admin.ModelAdmin): search_fields = ("name", "description") autocomplete_fields = ("cloud_provider",) actions = ["test_kubernetes_connection"] - ordering = ("name",) fieldsets = ( ( diff --git a/src/servala/core/models/service.py b/src/servala/core/models/service.py index baca840..82d00da 100644 --- a/src/servala/core/models/service.py +++ b/src/servala/core/models/service.py @@ -355,27 +355,11 @@ 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) @@ -384,10 +368,10 @@ class ControlPlaneCRD(ServalaModelMixin, models.Model): for crd in crds.items: if matching_crd: break - if crd.spec.group == self.group: + if crd.spec.group == group: for served_version in crd.spec.versions: - if served_version.name == self.version and served_version.served: - if crd.spec.names.kind == self.kind: + if served_version.name == version and served_version.served: + if crd.spec.names.kind == kind: matching_crd = crd break return matching_crd @@ -398,8 +382,9 @@ 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 == self.version: + if v.name == version: result = v.schema.open_apiv3_schema.to_dict() timeout_seconds = 60 * 60 * 24 cache.set(cache_key, result, timeout=timeout_seconds) @@ -410,9 +395,9 @@ class ControlPlaneCRD(ServalaModelMixin, models.Model): from servala.core.crd import generate_django_model kwargs = { - "group": self.group, - "version": self.version, - "kind": self.kind, + key: value + for key, value in self.service_definition.api_definition.items() + if key in ("group", "version", "kind") } return generate_django_model(self.resource_schema, **kwargs) @@ -532,9 +517,12 @@ 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"{context.group}/{context.version}", - "kind": context.kind, + "apiVersion": f"{group}/{version}", + "kind": kind, "metadata": { "name": name, "namespace": organization.namespace, @@ -546,11 +534,15 @@ 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=context.group, - version=context.version, + group=group, + version=version, namespace=organization.namespace, - plural=context.kind_plural, + plural=plural, body=create_data, ) except Exception as e: @@ -564,95 +556,3 @@ 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 - - @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 - - @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 [] - - @cached_property - def connection_credentials(self): - """ - Get connection credentials directly from the resource's writeConnectionSecretToRef - after checking that secret conditions are available. - """ - if not self.kubernetes_object: - return {} - - # Check if secrets are available based on conditions - secrets_available = any( - [ - condition.get("type") == "Ready" and condition.get("status") == "True" - for condition in self.status_conditions - ] - ) - if not secrets_available: - return {} - - spec = self.kubernetes_object.get("spec") - if not (secret_ref := spec.get("writeConnectionSecretToRef")): - return {} - if not (secret_name := secret_ref.get("name")): - return {} - - try: - # Get the secret data - v1 = kubernetes.client.CoreV1Api( - self.context.control_plane.get_kubernetes_client() - ) - secret = v1.read_namespaced_secret( - name=secret_name, namespace=self.organization.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 b9cf4dc..7a949f1 100644 --- a/src/servala/frontend/templates/frontend/organizations/service_instance_detail.html +++ b/src/servala/frontend/templates/frontend/organizations/service_instance_detail.html @@ -7,13 +7,11 @@ {% endblock html_title %} {% block content %}
-
-
-
-
-

{% translate "Details" %}

-
-
+
+
+
+
+
{% translate "Details" %}
{% translate "Service" %}
@@ -51,148 +49,6 @@
- {% if instance.status_conditions %} -
-
-
-

{% translate "Status" %}

-
-
-
-
- - - - - - - - - - - - {% 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 %} - {% if instance.spec %} -
-
-
-

{% translate "Specification" %}

-
-
-
-
- - - -
- {% 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 %} -
-
-
-
-
-
- {% 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 %} diff --git a/src/servala/frontend/templatetags/pprint_filters.py b/src/servala/frontend/templatetags/pprint_filters.py deleted file mode 100644 index e20772e..0000000 --- a/src/servala/frontend/templatetags/pprint_filters.py +++ /dev/null @@ -1,12 +0,0 @@ -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 diff --git a/src/servala/frontend/views/service.py b/src/servala/frontend/views/service.py index b6ecc42..6ee6b1e 100644 --- a/src/servala/frontend/views/service.py +++ b/src/servala/frontend/views/service.py @@ -4,7 +4,6 @@ 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, @@ -169,116 +168,6 @@ 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"