diff --git a/src/servala/core/crd.py b/src/servala/core/crd.py
index f356c7b..173b26c 100644
--- a/src/servala/core/crd.py
+++ b/src/servala/core/crd.py
@@ -173,11 +173,6 @@ class CrdModelFormMixin:
for field in ("organization", "context"):
self.fields[field].widget = forms.HiddenInput()
- if self.instance and self.instance.pk:
- self.fields["name"].disabled = True
- self.fields["name"].help_text = _("Name cannot be changed after creation.")
- self.fields["name"].widget = forms.HiddenInput()
-
def strip_title(self, field_name, label):
field = self.fields[field_name]
if field and field.label.startswith(label):
@@ -188,10 +183,7 @@ class CrdModelFormMixin:
# General fieldset for non-spec fields
general_fields = [
- field_name
- for field_name, field in self.fields.items()
- if not field_name.startswith("spec.")
- and not isinstance(field.widget, forms.HiddenInput)
+ field for field in self.fields if not field.startswith("spec.")
]
if general_fields:
fieldsets.append(
diff --git a/src/servala/core/models/service.py b/src/servala/core/models/service.py
index 901a822..c38cac0 100644
--- a/src/servala/core/models/service.py
+++ b/src/servala/core/models/service.py
@@ -188,10 +188,6 @@ class ControlPlane(ServalaModelMixin, models.Model):
def get_kubernetes_client(self):
return kubernetes.client.ApiClient(self.kubernetes_config)
- @cached_property
- def custom_objects_api(self):
- return client.CustomObjectsApi(self.get_kubernetes_client())
-
def test_connection(self):
if not self.api_credentials:
return False, _("No API credentials provided")
@@ -513,19 +509,6 @@ class ServiceInstance(ServalaModelMixin, models.Model):
class urls(urlman.Urls):
base = "{self.organization.urls.instances}{self.name}/"
- update = "{base}update/"
-
- def _clear_kubernetes_caches(self):
- """Clears cached properties that depend on Kubernetes state."""
- attrs = self.__dict__.keys()
- for key in (
- "kubernetes_object",
- "spec",
- "status_conditions",
- "connection_credentials",
- ):
- if key in attrs:
- delattr(self, key)
@classmethod
def create_instance(cls, name, organization, context, created_by, spec_data):
@@ -557,7 +540,9 @@ class ServiceInstance(ServalaModelMixin, models.Model):
}
if label := context.control_plane.required_label:
create_data["metadata"]["labels"] = {settings.DEFAULT_LABEL_KEY: label}
- api_instance = context.control_plane.custom_objects_api
+ api_instance = client.CustomObjectsApi(
+ context.control_plane.get_kubernetes_client()
+ )
api_instance.create_namespaced_custom_object(
group=context.group,
version=context.version,
@@ -577,47 +562,6 @@ class ServiceInstance(ServalaModelMixin, models.Model):
raise ValidationError(_("Error creating instance: {}").format(str(e)))
return instance
- def update_spec(self, spec_data, updated_by):
- try:
- api_instance = self.context.control_plane.custom_objects_api
- patch_body = {"spec": spec_data}
-
- api_instance.patch_namespaced_custom_object(
- group=self.context.group,
- version=self.context.version,
- namespace=self.organization.namespace,
- plural=self.context.kind_plural,
- name=self.name,
- body=patch_body,
- )
- self._clear_kubernetes_caches()
- self.save() # Updates updated_at timestamp
- except ApiException as e:
- if e.status == 404:
- raise ValidationError(
- _(
- "Service instance not found in Kubernetes. It may have been deleted externally."
- )
- )
- try:
- error_body = json.loads(e.body)
- reason = error_body.get("message", str(e))
- raise ValidationError(
- _("Kubernetes API error updating instance: {error}").format(
- error=reason
- )
- )
- except (ValueError, TypeError):
- raise ValidationError(
- _("Kubernetes API error updating instance: {error}").format(
- error=str(e)
- )
- )
- except Exception as e:
- raise ValidationError(
- _("Error updating instance: {error}").format(error=str(e))
- )
-
@cached_property
def kubernetes_object(self):
"""Fetch the Kubernetes custom resource object"""
@@ -651,20 +595,6 @@ class ServiceInstance(ServalaModelMixin, models.Model):
spec.pop("writeConnectionSecretToRef", None)
return spec
- @cached_property
- def spec_object(self):
- """Dynamically generated CRD object."""
- return self.context.django_model(
- name=self.name,
- organization=self.organization,
- context=self.context,
- spec=self.spec,
- # We pass -1 as ID in order to make it clear that a) this object exists (remotely),
- # and b) it’s not a normal database object. This allows us to treat e.g. update
- # forms differently from create forms.
- pk=-1,
- )
-
@cached_property
def status_conditions(self):
if not self.kubernetes_object:
diff --git a/src/servala/frontend/templates/frontend/base.html b/src/servala/frontend/templates/frontend/base.html
index 89d363f..f5ce6dd 100644
--- a/src/servala/frontend/templates/frontend/base.html
+++ b/src/servala/frontend/templates/frontend/base.html
@@ -25,13 +25,9 @@
-
- {% block page_title %}
- Dashboard
- {% endblock page_title %}
-
- {% block page_title_extra %}
- {% endblock page_title_extra %}
+ {% block page_title %}
+ Dashboard
+ {% endblock page_title %}
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 973d2f1..b9cf4dc 100644
--- a/src/servala/frontend/templates/frontend/organizations/service_instance_detail.html
+++ b/src/servala/frontend/templates/frontend/organizations/service_instance_detail.html
@@ -5,11 +5,6 @@
{{ instance.name }}
{% endblock page_title %}
{% endblock html_title %}
-{% block page_title_extra %}
- {% if has_change_permission %}
-
{% translate "Edit" %}
- {% endif %}
-{% endblock page_title_extra %}
{% block content %}
diff --git a/src/servala/frontend/templates/frontend/organizations/service_instance_update.html b/src/servala/frontend/templates/frontend/organizations/service_instance_update.html
deleted file mode 100644
index fa7f502..0000000
--- a/src/servala/frontend/templates/frontend/organizations/service_instance_update.html
+++ /dev/null
@@ -1,42 +0,0 @@
-{% extends "frontend/base.html" %}
-{% load i18n %}
-{% load static %}
-{% load partials %}
-{% block html_title %}
- {% block page_title %}
- {% block title %}
- {% blocktranslate with instance_name=instance.name organization_name=request.organization.name %}Update {{ instance_name }} in {{ organization_name }}{% endblocktranslate %}
- {% endblock %}
- {% endblock page_title %}
-{% endblock html_title %}
-{% block page_title_extra %}
-
{% translate "Back" %}
-{% endblock page_title_extra %}
-{% partialdef service-form %}
-{% if form %}
-
-
-
- {% if form_error %}
-
- {% translate "Oops! Something went wrong with the service form generation. Please try again later." %}
-
- {% else %}
- {% include "includes/tabbed_fieldset_form.html" with form=form %}
- {% endif %}
-
-
-{% endif %}
-{% endpartialdef %}
-{% block content %}
-
-
- {% if not form %}
-
- {% translate "Cannot update this service instance because its details could not be retrieved from the underlying system. It might have been deleted externally." %}
-
- {% else %}
-
{% partial service-form %}
- {% endif %}
-
- {% endblock %}
diff --git a/src/servala/frontend/urls.py b/src/servala/frontend/urls.py
index 64c84bf..2838dae 100644
--- a/src/servala/frontend/urls.py
+++ b/src/servala/frontend/urls.py
@@ -50,11 +50,6 @@ urlpatterns = [
views.ServiceInstanceDetailView.as_view(),
name="organization.instance",
),
- path(
- "instances/
/update/",
- views.ServiceInstanceUpdateView.as_view(),
- name="organization.instance.update",
- ),
]
),
),
diff --git a/src/servala/frontend/views/__init__.py b/src/servala/frontend/views/__init__.py
index d13d4d5..5de38ee 100644
--- a/src/servala/frontend/views/__init__.py
+++ b/src/servala/frontend/views/__init__.py
@@ -9,7 +9,6 @@ from .service import (
ServiceDetailView,
ServiceInstanceDetailView,
ServiceInstanceListView,
- ServiceInstanceUpdateView,
ServiceListView,
ServiceOfferingDetailView,
)
@@ -23,7 +22,6 @@ __all__ = [
"ServiceDetailView",
"ServiceInstanceDetailView",
"ServiceInstanceListView",
- "ServiceInstanceUpdateView",
"ServiceListView",
"ServiceOfferingDetailView",
"ProfileView",
diff --git a/src/servala/frontend/views/service.py b/src/servala/frontend/views/service.py
index 298d5bc..b6ecc42 100644
--- a/src/servala/frontend/views/service.py
+++ b/src/servala/frontend/views/service.py
@@ -2,7 +2,6 @@ from django.contrib import messages
from django.core.exceptions import ValidationError
from django.shortcuts import redirect
from django.utils.functional import cached_property
-from django.utils.translation import gettext_lazy as _
from django.views.generic import DetailView, ListView
from servala.core.crd import deslugify
@@ -17,11 +16,7 @@ from servala.frontend.forms.service import (
ServiceFilterForm,
ServiceInstanceFilterForm,
)
-from servala.frontend.views.mixins import (
- HtmxUpdateView,
- HtmxViewMixin,
- OrganizationViewMixin,
-)
+from servala.frontend.views.mixins import HtmxViewMixin, OrganizationViewMixin
class ServiceListView(OrganizationViewMixin, ListView):
@@ -110,8 +105,6 @@ class ServiceOfferingDetailView(OrganizationViewMixin, HtmxViewMixin, DetailView
).first()
def get_instance_form(self):
- if not self.context_object:
- return None
return self.context_object.model_form_class(
data=self.request.POST if self.request.method == "POST" else None,
initial={
@@ -137,10 +130,6 @@ class ServiceOfferingDetailView(OrganizationViewMixin, HtmxViewMixin, DetailView
return self.render_to_response(context)
form = self.get_instance_form()
- if not form: # Should not happen if context_object is valid, but as a safeguard
- messages.error(self.request, _("Could not initialize service form."))
- return self.render_to_response(context)
-
if form.is_valid():
try:
service_instance = ServiceInstance.create_instance(
@@ -154,23 +143,21 @@ class ServiceOfferingDetailView(OrganizationViewMixin, HtmxViewMixin, DetailView
except ValidationError as e:
messages.error(self.request, e.message or str(e))
except Exception as e:
- messages.error(
- self.request, _("Error creating instance: {}").format(str(e))
- )
+ messages.error(self.request, str(e))
# If the form is not valid or if the service creation failed, we render it again
context["service_form"] = form
return self.render_to_response(context)
-class ServiceInstanceMixin:
- model = ServiceInstance
- context_object_name = "instance"
- slug_field = "name"
+class ServiceInstanceDetailView(OrganizationViewMixin, DetailView):
+ """View to display details of a specific service instance."""
- def dispatch(self, *args, **kwargs):
- self._has_warned = False
- return super().dispatch(*args, **kwargs)
+ template_name = "frontend/organizations/service_instance_detail.html"
+ context_object_name = "instance"
+ model = ServiceInstance
+ permission_type = "view"
+ slug_field = "name"
def get_queryset(self):
"""Return service instance for the current organization."""
@@ -182,39 +169,6 @@ class ServiceInstanceMixin:
"context__service_definition__service",
)
- def get_object(self, **kwargs):
- instance = super().get_object(**kwargs)
- if (
- not instance.kubernetes_object
- and not instance.is_deleted
- and not self._has_warned
- ):
- messages.warning(
- self.request,
- _(
- "Could not retrieve instance details from Kubernetes. It might have been deleted externally."
- ),
- )
- self._has_warned = True
- return instance
-
-
-class ServiceInstanceDetailView(
- ServiceInstanceMixin, OrganizationViewMixin, DetailView
-):
- template_name = "frontend/organizations/service_instance_detail.html"
- permission_type = "view"
-
- def get_context_data(self, **kwargs):
- context = super().get_context_data(**kwargs)
- if self.object.kubernetes_object and self.object.spec:
- context["spec_fieldsets"] = self.get_nested_spec()
- permission_required = ServiceInstance.get_perm("change")
- context["has_change_permission"] = self.request.user.has_perm(
- permission_required, self.object
- )
- return context
-
def get_nested_spec(self):
"""
Organize spec data into fieldsets similar to how the form does it.
@@ -318,43 +272,12 @@ class ServiceInstanceDetailView(
return fieldsets
-
-class ServiceInstanceUpdateView(
- ServiceInstanceMixin, OrganizationViewMixin, HtmxUpdateView
-):
- template_name = "frontend/organizations/service_instance_update.html"
- permission_type = "change"
-
- def get_form_class(self):
- return self.object.context.model_form_class
-
- def get_form_kwargs(self):
- kwargs = super().get_form_kwargs()
- kwargs["instance"] = self.object.spec_object
- return kwargs
-
- def form_valid(self, form):
- try:
- spec_data = form.get_nested_data().get("spec")
- self.object.update_spec(spec_data=spec_data, updated_by=self.request.user)
- messages.success(
- self.request,
- _("Service instance '{name}' updated successfully.").format(
- name=self.object.name
- ),
- )
- return redirect(self.object.urls.base)
- except ValidationError as e:
- messages.error(self.request, e.message or str(e))
- return self.form_invalid(form)
- except Exception as e:
- messages.error(
- self.request, _("Error updating instance: {error}").format(error=str(e))
- )
- return self.form_invalid(form)
-
- def get_success_url(self):
- return self.object.urls.base
+ 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):
@@ -370,12 +293,7 @@ class ServiceInstanceListView(OrganizationViewMixin, ListView):
def get_queryset(self):
"""Return all service instances for the current organization with filtering."""
queryset = ServiceInstance.objects.filter(
- organization=self.request.organization,
- is_deleted=False, # Exclude soft-deleted
- ).select_related(
- "context__service_offering__provider",
- "context__control_plane",
- "context__service_definition__service",
+ organization=self.request.organization
)
if self.filter_form.is_valid():
queryset = self.filter_form.filter_queryset(queryset)
diff --git a/src/servala/static/css/servala.css b/src/servala/static/css/servala.css
index e607205..e2eb044 100644
--- a/src/servala/static/css/servala.css
+++ b/src/servala/static/css/servala.css
@@ -85,8 +85,3 @@ html[data-bs-theme="dark"] .btn-outline-primary, .btn-outline-primary {
a.btn-keycloak {
display: inline-flex;
}
-.page-heading h3 {
- display: flex;
- flex-wrap: wrap;
- justify-content: space-between;
-}