Compare commits
9 commits
55f6169562
...
2a359b50ef
Author | SHA1 | Date | |
---|---|---|---|
2a359b50ef | |||
60b47ed6c8 | |||
40811cbc08 | |||
7afc4400b7 | |||
93916cdcbc | |||
6d34e3abdc | |||
912842bd82 | |||
c4522e31e8 | |||
0eb07feeef |
3 changed files with 281 additions and 28 deletions
|
@ -355,11 +355,27 @@ class ControlPlaneCRD(ServalaModelMixin, models.Model):
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.service_offering} on {self.control_plane} with {self.service_definition}"
|
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
|
@cached_property
|
||||||
def resource_definition(self):
|
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()
|
client = self.control_plane.get_kubernetes_client()
|
||||||
|
|
||||||
extensions_api = kubernetes.client.ApiextensionsV1Api(client)
|
extensions_api = kubernetes.client.ApiextensionsV1Api(client)
|
||||||
|
@ -368,10 +384,10 @@ class ControlPlaneCRD(ServalaModelMixin, models.Model):
|
||||||
for crd in crds.items:
|
for crd in crds.items:
|
||||||
if matching_crd:
|
if matching_crd:
|
||||||
break
|
break
|
||||||
if crd.spec.group == group:
|
if crd.spec.group == self.group:
|
||||||
for served_version in crd.spec.versions:
|
for served_version in crd.spec.versions:
|
||||||
if served_version.name == version and served_version.served:
|
if served_version.name == self.version and served_version.served:
|
||||||
if crd.spec.names.kind == kind:
|
if crd.spec.names.kind == self.kind:
|
||||||
matching_crd = crd
|
matching_crd = crd
|
||||||
break
|
break
|
||||||
return matching_crd
|
return matching_crd
|
||||||
|
@ -382,9 +398,8 @@ class ControlPlaneCRD(ServalaModelMixin, models.Model):
|
||||||
if result := cache.get(cache_key):
|
if result := cache.get(cache_key):
|
||||||
return result
|
return result
|
||||||
|
|
||||||
version = self.service_definition.api_definition["version"]
|
|
||||||
for v in self.resource_definition.spec.versions:
|
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()
|
result = v.schema.open_apiv3_schema.to_dict()
|
||||||
timeout_seconds = 60 * 60 * 24
|
timeout_seconds = 60 * 60 * 24
|
||||||
cache.set(cache_key, result, timeout=timeout_seconds)
|
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
|
from servala.core.crd import generate_django_model
|
||||||
|
|
||||||
kwargs = {
|
kwargs = {
|
||||||
key: value
|
"group": self.group,
|
||||||
for key, value in self.service_definition.api_definition.items()
|
"version": self.version,
|
||||||
if key in ("group", "version", "kind")
|
"kind": self.kind,
|
||||||
}
|
}
|
||||||
return generate_django_model(self.resource_schema, **kwargs)
|
return generate_django_model(self.resource_schema, **kwargs)
|
||||||
|
|
||||||
|
@ -517,12 +532,9 @@ class ServiceInstance(ServalaModelMixin, models.Model):
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
group = context.service_definition.api_definition["group"]
|
|
||||||
version = context.service_definition.api_definition["version"]
|
|
||||||
kind = context.service_definition.api_definition["kind"]
|
|
||||||
create_data = {
|
create_data = {
|
||||||
"apiVersion": f"{group}/{version}",
|
"apiVersion": f"{context.group}/{context.version}",
|
||||||
"kind": kind,
|
"kind": context.kind,
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"name": name,
|
"name": name,
|
||||||
"namespace": organization.namespace,
|
"namespace": organization.namespace,
|
||||||
|
@ -534,15 +546,11 @@ class ServiceInstance(ServalaModelMixin, models.Model):
|
||||||
api_instance = client.CustomObjectsApi(
|
api_instance = client.CustomObjectsApi(
|
||||||
context.control_plane.get_kubernetes_client()
|
context.control_plane.get_kubernetes_client()
|
||||||
)
|
)
|
||||||
plural = kind.lower()
|
|
||||||
if not plural.endswith("s"):
|
|
||||||
plural = f"{plural}s"
|
|
||||||
|
|
||||||
api_instance.create_namespaced_custom_object(
|
api_instance.create_namespaced_custom_object(
|
||||||
group=group,
|
group=context.group,
|
||||||
version=version,
|
version=context.version,
|
||||||
namespace=organization.namespace,
|
namespace=organization.namespace,
|
||||||
plural=plural,
|
plural=context.kind_plural,
|
||||||
body=create_data,
|
body=create_data,
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
@ -556,3 +564,121 @@ class ServiceInstance(ServalaModelMixin, models.Model):
|
||||||
raise ValidationError(_("Kubernetes API error: {}").format(str(e)))
|
raise ValidationError(_("Kubernetes API error: {}").format(str(e)))
|
||||||
raise ValidationError(_("Error creating instance: {}").format(str(e)))
|
raise ValidationError(_("Error creating instance: {}").format(str(e)))
|
||||||
return instance
|
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 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"<binary data: {len(value)} bytes>"
|
||||||
|
|
||||||
|
return credentials
|
||||||
|
except ApiException as e:
|
||||||
|
return {"error": str(e)}
|
||||||
|
except Exception as e:
|
||||||
|
return {"error": str(e)}
|
||||||
|
|
|
@ -7,11 +7,13 @@
|
||||||
{% endblock html_title %}
|
{% endblock html_title %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<section class="section">
|
<section class="section">
|
||||||
<div class="card">
|
<div class="row">
|
||||||
<div class="card-body">
|
<div class="col-12 col-md-4">
|
||||||
<div class="row">
|
<div class="card">
|
||||||
<div class="col-md-6">
|
<div class="card-header">
|
||||||
<h5>{% translate "Details" %}</h5>
|
<h4>{% translate "Details" %}</h4>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
<dl class="row">
|
<dl class="row">
|
||||||
<dt class="col-sm-4">{% translate "Service" %}</dt>
|
<dt class="col-sm-4">{% translate "Service" %}</dt>
|
||||||
<dd class="col-sm-8">
|
<dd class="col-sm-8">
|
||||||
|
@ -49,6 +51,119 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% if instance.status_conditions %}
|
||||||
|
<div class="col-12 col-md-8">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h4>{% translate "Status" %}</h4>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-bordered">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{% translate "Type" %}</th>
|
||||||
|
<th>{% translate "Status" %}</th>
|
||||||
|
<th>{% translate "Last Transition Time" %}</th>
|
||||||
|
<th>{% translate "Reason" %}</th>
|
||||||
|
<th>{% translate "Message" %}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for condition in instance.status_conditions %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ condition.type }}</td>
|
||||||
|
<td>
|
||||||
|
{% if condition.status == "True" %}
|
||||||
|
<span class="badge text-bg-success">True</span>
|
||||||
|
{% elif condition.status == "False" %}
|
||||||
|
<span class="badge text-bg-danger">False</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge text-bg-secondary">{{ condition.status }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>{{ condition.lastTransitionTime }}</td>
|
||||||
|
<td>{{ condition.reason }}</td>
|
||||||
|
<td>{{ condition.message }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if instance.spec %}
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<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>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if instance.connection_credentials %}
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h4>{% translate "Connection Credentials" %}</h4>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-bordered">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{% translate "Name" %}</th>
|
||||||
|
<th>{% translate "Value" %}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for key, value in instance.connection_credentials.items %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ key }}</td>
|
||||||
|
<td>
|
||||||
|
{% if key == "error" %}
|
||||||
|
<span class="text-danger">{{ value }}</span>
|
||||||
|
{% else %}
|
||||||
|
<code>{{ value }}</code>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
|
12
src/servala/frontend/templatetags/pprint_filters.py
Normal file
12
src/servala/frontend/templatetags/pprint_filters.py
Normal file
|
@ -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
|
Loading…
Add table
Add a link
Reference in a new issue