Compare commits

..

2 commits

Author SHA1 Message Date
b2d9004359 Display spec data tabbed like in form
All checks were successful
Tests / test (push) Successful in 26s
2025-04-17 10:47:08 +02:00
6160f48d61 Possibly fix secret retrieval (untested) 2025-04-17 10:20:22 +02:00
4 changed files with 180 additions and 67 deletions

View file

@ -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"

View file

@ -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

View file

@ -103,29 +103,58 @@
<h4>{% translate "Specification" %}</h4>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-bordered">
<thead>
<tr>
<th>{% translate "Property" %}</th>
<th>{% translate "Value" %}</th>
</tr>
</thead>
<tbody>
{% for key, value in instance.spec.items %}
<tr>
<td>{{ key }}</td>
<td>
{% if value|default:""|stringformat:"s"|slice:":1" == "{" or value|default:""|stringformat:"s"|slice:":1" == "[" %}
<pre>{{ value|pprint }}</pre>
{% else %}
{{ value }}
{% endif %}
</td>
</tr>
<div class="row">
<div class="col-12">
<!-- Tabs -->
<ul class="nav nav-tabs" role="tablist">
{% for fieldset in spec_fieldsets %}
<li class="nav-item" role="presentation">
<a href="#spec-tab-{{ forloop.counter }}"
class="nav-link {% if forloop.first %}active{% endif %}"
data-bs-toggle="tab"
role="tab">{{ fieldset.title }}</a>
</li>
{% endfor %}
</tbody>
</table>
</ul>
<!-- Tab Content -->
<div class="tab-content pt-3">
{% for fieldset in spec_fieldsets %}
<div class="tab-pane fade {% if forloop.first %}show active{% endif %}"
id="spec-tab-{{ forloop.counter }}"
role="tabpanel">
<!-- Main Fields -->
<dl class="row">
{% for field in fieldset.fields %}
<dt class="col-sm-3">{{ field.label }}</dt>
<dd class="col-sm-9">
{% if field.value|default:""|stringformat:"s"|slice:":1" == "{" or field.value|default:""|stringformat:"s"|slice:":1" == "[" %}
<pre>{{ field.value|pprint }}</pre>
{% else %}
{{ field.value }}
{% endif %}
</dd>
{% endfor %}
</dl>
<!-- Nested Fieldsets -->
{% for sub_key, sub_fieldset in fieldset.fieldsets.items %}
<h5>{{ sub_fieldset.title }}</h5>
<dl class="row">
{% for field in sub_fieldset.fields %}
<dt class="col-sm-3">{{ field.label }}</dt>
<dd class="col-sm-9">
{% if field.value|default:""|stringformat:"s"|slice:":1" == "{" or field.value|default:""|stringformat:"s"|slice:":1" == "[" %}
<pre>{{ field.value|pprint }}</pre>
{% else %}
{{ field.value }}
{% endif %}
</dd>
{% endfor %}
</dl>
{% endfor %}
</div>
{% endfor %}
</div>
</div>
</div>
</div>
</div>

View file

@ -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"