Implement service instance delete basics
This commit is contained in:
parent
926c9441f2
commit
09ce5406ee
6 changed files with 142 additions and 10 deletions
|
@ -7,6 +7,7 @@ from django.conf import settings
|
|||
from django.core.cache import cache
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import IntegrityError, models
|
||||
from django.utils import timezone
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from encrypted_fields.fields import EncryptedJSONField
|
||||
|
@ -373,7 +374,13 @@ class ControlPlaneCRD(ServalaModelMixin, models.Model):
|
|||
|
||||
@cached_property
|
||||
def kind_plural(self):
|
||||
return self.resource_definition.status.accepted_names.plural
|
||||
if hasattr(self.resource_definition, 'status') and \
|
||||
hasattr(self.resource_definition.status, 'accepted_names') and \
|
||||
self.resource_definition.status.accepted_names:
|
||||
return self.resource_definition.status.accepted_names.plural
|
||||
if self.kind.endswith("s"):
|
||||
return self.kind.lower()
|
||||
return f"{self.kind.lower()}s"
|
||||
|
||||
@cached_property
|
||||
def resource_definition(self):
|
||||
|
@ -399,8 +406,13 @@ class ControlPlaneCRD(ServalaModelMixin, models.Model):
|
|||
if result := cache.get(cache_key):
|
||||
return result
|
||||
|
||||
if not self.resource_definition:
|
||||
return
|
||||
|
||||
for v in self.resource_definition.spec.versions:
|
||||
if v.name == self.version:
|
||||
if not v.schema or not v.schema.open_apiv3_schema:
|
||||
return
|
||||
result = v.schema.open_apiv3_schema.to_dict()
|
||||
timeout_seconds = 60 * 60 * 24
|
||||
cache.set(cache_key, result, timeout=timeout_seconds)
|
||||
|
@ -410,6 +422,9 @@ class ControlPlaneCRD(ServalaModelMixin, models.Model):
|
|||
def django_model(self):
|
||||
from servala.core.crd import generate_django_model
|
||||
|
||||
if not self.resource_schema:
|
||||
return
|
||||
|
||||
kwargs = {
|
||||
"group": self.group,
|
||||
"version": self.version,
|
||||
|
@ -421,6 +436,8 @@ class ControlPlaneCRD(ServalaModelMixin, models.Model):
|
|||
def model_form_class(self):
|
||||
from servala.core.crd import generate_model_form_class
|
||||
|
||||
if not self.django_model:
|
||||
return
|
||||
return generate_model_form_class(self.django_model)
|
||||
|
||||
|
||||
|
@ -514,6 +531,7 @@ class ServiceInstance(ServalaModelMixin, models.Model):
|
|||
class urls(urlman.Urls):
|
||||
base = "{self.organization.urls.instances}{self.name}/"
|
||||
update = "{base}update/"
|
||||
delete = "{base}delete/"
|
||||
|
||||
def _clear_kubernetes_caches(self):
|
||||
"""Clears cached properties that depend on Kubernetes state."""
|
||||
|
@ -618,9 +636,41 @@ class ServiceInstance(ServalaModelMixin, models.Model):
|
|||
_("Error updating instance: {error}").format(error=str(e))
|
||||
)
|
||||
|
||||
def delete_instance(self, user):
|
||||
"""
|
||||
Soft deletes the instance in Django and initiates deletion of the
|
||||
corresponding Kubernetes custom resource.
|
||||
"""
|
||||
if self.is_deleted:
|
||||
return
|
||||
|
||||
self.is_deleted = True
|
||||
self.deleted_at = timezone.now()
|
||||
self.deleted_by = user
|
||||
self.save(update_fields=["is_deleted", "deleted_at", "deleted_by", "updated_at"])
|
||||
|
||||
self._clear_kubernetes_caches()
|
||||
|
||||
try:
|
||||
api_instance = self.context.control_plane.custom_objects_api
|
||||
api_instance.delete_namespaced_custom_object(
|
||||
group=self.context.group,
|
||||
version=self.context.version,
|
||||
namespace=self.organization.namespace,
|
||||
plural=self.context.kind_plural,
|
||||
name=self.name,
|
||||
body=client.V1DeleteOptions(),
|
||||
)
|
||||
except ApiException as e:
|
||||
if e.status != 404:
|
||||
# 404 is fine, the object was deleted already.
|
||||
raise
|
||||
|
||||
@cached_property
|
||||
def kubernetes_object(self):
|
||||
"""Fetch the Kubernetes custom resource object"""
|
||||
if self.is_deleted:
|
||||
return
|
||||
try:
|
||||
api_instance = client.CustomObjectsApi(
|
||||
self.context.control_plane.get_kubernetes_client()
|
||||
|
@ -654,6 +704,8 @@ class ServiceInstance(ServalaModelMixin, models.Model):
|
|||
@cached_property
|
||||
def spec_object(self):
|
||||
"""Dynamically generated CRD object."""
|
||||
if not self.context.django_model:
|
||||
return
|
||||
return self.context.django_model(
|
||||
name=self.name,
|
||||
organization=self.organization,
|
||||
|
|
|
@ -86,3 +86,35 @@ class ServiceInstanceFilterForm(forms.Form):
|
|||
else:
|
||||
queryset = queryset.filter(is_deleted=True)
|
||||
return queryset
|
||||
|
||||
|
||||
class ServiceInstanceDeleteForm(forms.Form):
|
||||
name_confirm = forms.CharField(
|
||||
label=_("Instance Name"),
|
||||
max_length=63,
|
||||
widget=forms.TextInput(attrs={"class": "form-control"}),
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.instance_name = kwargs.pop("instance_name", None)
|
||||
super().__init__(*args, **kwargs)
|
||||
if self.instance_name:
|
||||
self.fields["name_confirm"].help_text = _(
|
||||
"To confirm deletion, please type the instance name: <strong>{instance_name}</strong>"
|
||||
).format(instance_name=self.instance_name)
|
||||
else:
|
||||
self.fields["name_confirm"].help_text = _(
|
||||
"Please type the instance name to confirm deletion."
|
||||
)
|
||||
|
||||
def clean_name_confirm(self):
|
||||
entered_name = self.cleaned_data.get("name_confirm")
|
||||
if not self.instance_name:
|
||||
raise ValidationError(
|
||||
_("Cannot confirm deletion: instance name not provided to form.")
|
||||
)
|
||||
if entered_name != self.instance_name:
|
||||
raise ValidationError(
|
||||
_("The entered name does not match the instance name. Deletion not confirmed.")
|
||||
)
|
||||
return entered_name
|
||||
|
|
|
@ -55,6 +55,11 @@ urlpatterns = [
|
|||
views.ServiceInstanceUpdateView.as_view(),
|
||||
name="organization.instance.update",
|
||||
),
|
||||
path(
|
||||
"instances/<slug:slug>/delete/",
|
||||
views.ServiceInstanceDeleteView.as_view(),
|
||||
name="organization.instance.delete",
|
||||
),
|
||||
]
|
||||
),
|
||||
),
|
||||
|
|
|
@ -7,6 +7,7 @@ from .organization import (
|
|||
)
|
||||
from .service import (
|
||||
ServiceDetailView,
|
||||
ServiceInstanceDeleteView,
|
||||
ServiceInstanceDetailView,
|
||||
ServiceInstanceListView,
|
||||
ServiceInstanceUpdateView,
|
||||
|
@ -21,6 +22,7 @@ __all__ = [
|
|||
"OrganizationDashboardView",
|
||||
"OrganizationUpdateView",
|
||||
"ServiceDetailView",
|
||||
"ServiceInstanceDeleteView",
|
||||
"ServiceInstanceDetailView",
|
||||
"ServiceInstanceListView",
|
||||
"ServiceInstanceUpdateView",
|
||||
|
|
|
@ -3,7 +3,7 @@ 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 django.views.generic import DetailView, UpdateView, ListView
|
||||
|
||||
from servala.core.crd import deslugify
|
||||
from servala.core.models import (
|
||||
|
@ -15,6 +15,7 @@ from servala.core.models import (
|
|||
from servala.frontend.forms.service import (
|
||||
ControlPlaneSelectForm,
|
||||
ServiceFilterForm,
|
||||
ServiceInstanceDeleteForm,
|
||||
ServiceInstanceFilterForm,
|
||||
)
|
||||
from servala.frontend.views.mixins import (
|
||||
|
@ -144,7 +145,7 @@ class ServiceOfferingDetailView(OrganizationViewMixin, HtmxViewMixin, DetailView
|
|||
if form.is_valid():
|
||||
try:
|
||||
service_instance = ServiceInstance.create_instance(
|
||||
organization=self.organization,
|
||||
organization=self.request.organization,
|
||||
name=form.cleaned_data["name"],
|
||||
context=self.context_object,
|
||||
created_by=request.user,
|
||||
|
@ -185,8 +186,8 @@ class ServiceInstanceMixin:
|
|||
def get_object(self, **kwargs):
|
||||
instance = super().get_object(**kwargs)
|
||||
if (
|
||||
not instance.kubernetes_object
|
||||
and not instance.is_deleted
|
||||
not instance.is_deleted
|
||||
and not instance.kubernetes_object
|
||||
and not self._has_warned
|
||||
):
|
||||
messages.warning(
|
||||
|
@ -207,11 +208,13 @@ class ServiceInstanceDetailView(
|
|||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
if self.object.kubernetes_object and self.object.spec:
|
||||
if not self.object.is_deleted and 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
|
||||
ServiceInstance.get_perm("change"), self.object
|
||||
)
|
||||
context["has_delete_permission"] = self.request.user.has_perm(
|
||||
ServiceInstance.get_perm("delete"), self.object
|
||||
)
|
||||
return context
|
||||
|
||||
|
@ -220,8 +223,9 @@ class ServiceInstanceDetailView(
|
|||
Organize spec data into fieldsets similar to how the form does it.
|
||||
"""
|
||||
spec = self.object.spec or {}
|
||||
if not spec:
|
||||
return []
|
||||
|
||||
# Process spec fields
|
||||
others = []
|
||||
nested_fieldsets = {}
|
||||
|
||||
|
@ -379,10 +383,47 @@ class ServiceInstanceListView(OrganizationViewMixin, ListView):
|
|||
)
|
||||
if self.filter_form.is_valid():
|
||||
queryset = self.filter_form.filter_queryset(queryset)
|
||||
return queryset
|
||||
status_filter = self.filter_form.cleaned_data.get("status") if self.filter_form.is_valid() else "active"
|
||||
if status_filter == "active":
|
||||
queryset = queryset.filter(is_deleted=False)
|
||||
elif status_filter == "deleted":
|
||||
queryset = queryset.filter(is_deleted=True)
|
||||
return queryset.order_by("-created_at")
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["organization"] = self.request.organization
|
||||
context["filter_form"] = self.filter_form
|
||||
return context
|
||||
|
||||
|
||||
class ServiceInstanceDeleteView(
|
||||
ServiceInstanceMixin, OrganizationViewMixin, HtmxViewMixin, UpdateView
|
||||
):
|
||||
template_name = "frontend/organizations/service_instance_delete_form.html"
|
||||
form_class = ServiceInstanceDeleteForm
|
||||
permission_type = "delete"
|
||||
|
||||
def form_valid(self, form):
|
||||
try:
|
||||
self.object.delete_instance(user=self.request.user)
|
||||
messages.success(
|
||||
self.request,
|
||||
_("Service instance '{name}' has been scheduled for deletion.").format(
|
||||
name=self.object.name
|
||||
),
|
||||
)
|
||||
response = HttpResponse()
|
||||
response["HX-Redirect"] = self.get_success_url()
|
||||
return response
|
||||
except Exception as e:
|
||||
messages.error(
|
||||
self.request,
|
||||
_("An error occurred while trying to delete instance '{name}': {error}").format(
|
||||
name=self.object.name, error=str(e)
|
||||
),
|
||||
)
|
||||
return self.form_invalid(form)
|
||||
|
||||
def get_success_url(self):
|
||||
return self.request.organization.urls.instances
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue