Compare commits
2 commits
c8eaa99d38
...
b2d9004359
Author | SHA1 | Date | |
---|---|---|---|
b2d9004359 | |||
6160f48d61 |
4 changed files with 180 additions and 67 deletions
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue