diff --git a/pyproject.toml b/pyproject.toml index d881deb..b89d892 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,6 +9,7 @@ dependencies = [ "cryptography>=45.0.7", "django==5.2.6", "django-allauth>=65.10.0", + "django-auditlog>=3.2.1", "django-fernet-encrypted-fields>=0.3.0", "django-jsonform>=2.23.2", "django-scopes>=2.0.0", diff --git a/src/servala/core/admin.py b/src/servala/core/admin.py index ed07574..149635c 100644 --- a/src/servala/core/admin.py +++ b/src/servala/core/admin.py @@ -272,8 +272,8 @@ class ControlPlaneCRDAdmin(admin.ModelAdmin): @admin.register(ServiceInstance) class ServiceInstanceAdmin(admin.ModelAdmin): - list_display = ("name", "organization", "context", "created_by", "is_deleted") - list_filter = ("organization", "context", "is_deleted") + list_display = ("name", "organization", "context", "created_by") + list_filter = ("organization", "context") search_fields = ( "name", "organization__name", @@ -296,9 +296,6 @@ class ServiceInstanceAdmin(admin.ModelAdmin): "organization", "context", "created_by", - "is_deleted", - "deleted_at", - "deleted_by", ) }, ), diff --git a/src/servala/core/migrations/0006_remove_service_instance_soft_delete.py b/src/servala/core/migrations/0006_remove_service_instance_soft_delete.py new file mode 100644 index 0000000..0fcc02a --- /dev/null +++ b/src/servala/core/migrations/0006_remove_service_instance_soft_delete.py @@ -0,0 +1,25 @@ +# Generated by Django 5.2.4 on 2025-09-03 23:08 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("core", "0005_organization_sale_order_fields"), + ] + + operations = [ + migrations.RemoveField( + model_name="serviceinstance", + name="deleted_at", + ), + migrations.RemoveField( + model_name="serviceinstance", + name="deleted_by", + ), + migrations.RemoveField( + model_name="serviceinstance", + name="is_deleted", + ), + ] diff --git a/src/servala/core/models/service.py b/src/servala/core/models/service.py index a06db4e..6944750 100644 --- a/src/servala/core/models/service.py +++ b/src/servala/core/models/service.py @@ -6,11 +6,11 @@ import re import kubernetes import rules import urlman +from auditlog.registry import auditlog from django.conf import settings from django.core.cache import cache from django.core.exceptions import ValidationError from django.db import IntegrityError, models, transaction -from django.utils import timezone from django.utils.functional import cached_property from django.utils.safestring import mark_safe from django.utils.translation import gettext_lazy as _ @@ -571,16 +571,6 @@ class ServiceInstance(ServalaModelMixin, models.Model): on_delete=models.PROTECT, ) - is_deleted = models.BooleanField(default=False) - deleted_at = models.DateTimeField(null=True, blank=True) - deleted_by = models.ForeignKey( - to="core.User", - on_delete=models.SET_NULL, - null=True, - blank=True, - related_name="+", - ) - class Meta: verbose_name = _("Service instance") verbose_name_plural = _("Service instances") @@ -794,14 +784,10 @@ class ServiceInstance(ServalaModelMixin, models.Model): raise ValidationError(self.organization.add_support_message(message)) @transaction.atomic - def delete_instance(self, user): + def delete(self, using=None, keep_parents=False, user=None): """ - Soft deletes the instance in Django and initiates deletion of the - corresponding Kubernetes custom resource. + Deletes the Django instance and the corresponding Kubernetes custom resource. """ - if self.is_deleted: - return - if ( self.spec.get("parameters", {}) .get("security", {}) @@ -825,19 +811,13 @@ class ServiceInstance(ServalaModelMixin, models.Model): if e.status != 404: # 404 is fine, the object was deleted already. raise - 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() + return super().delete(using=using, keep_parents=keep_parents) @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() @@ -942,3 +922,6 @@ class ServiceInstance(ServalaModelMixin, models.Model): return {"error": str(e)} except Exception as e: return {"error": str(e)} + + +auditlog.register(ServiceInstance, exclude_fields=["updated_at"], serialize_data=True) diff --git a/src/servala/frontend/forms/service.py b/src/servala/frontend/forms/service.py index ca84ab3..5dd78a7 100644 --- a/src/servala/frontend/forms/service.py +++ b/src/servala/frontend/forms/service.py @@ -64,14 +64,6 @@ class ServiceInstanceFilterForm(forms.Form): required=False, label=_("Service Provider Zone"), ) - status = forms.ChoiceField( - choices=( - ("active", _("Active")), - ("deleted", _("Deleted")), - ), - required=False, - label=_("Status"), - ) def filter_queryset(self, queryset): if self.is_valid(): @@ -88,11 +80,6 @@ class ServiceInstanceFilterForm(forms.Form): ) if data.get("control_plane"): queryset = queryset.filter(context__control_plane=data["control_plane"]) - status = data.get("status") - if status == "active": - queryset = queryset.filter(is_deleted=False) - elif status == "deleted": - queryset = queryset.filter(is_deleted=True) return queryset diff --git a/src/servala/frontend/templates/frontend/organizations/dashboard.html b/src/servala/frontend/templates/frontend/organizations/dashboard.html index b0dd273..01faa69 100644 --- a/src/servala/frontend/templates/frontend/organizations/dashboard.html +++ b/src/servala/frontend/templates/frontend/organizations/dashboard.html @@ -112,13 +112,6 @@ {{ instance.context.service_offering.service.name }} - - {% if instance.is_deleted %} - {% translate "Deleted" %} - {% else %} - {% translate "Active" %} - {% endif %} - {{ instance.created_at|date:"M d, Y" }} 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 4c8ebf1..a12efcf 100644 --- a/src/servala/frontend/templates/frontend/organizations/service_instance_detail.html +++ b/src/servala/frontend/templates/frontend/organizations/service_instance_detail.html @@ -7,10 +7,10 @@ {% endblock html_title %} {% block page_title_extra %}
- {% if has_change_permission and not instance.is_deleted %} + {% if has_change_permission %} {% translate "Edit" %} {% endif %} - {% if has_delete_permission and not instance.is_deleted %} + {% if has_delete_permission %}
- {% if not instance.is_deleted and instance.status_conditions %} + {% if instance.status_conditions %}
@@ -118,7 +103,7 @@
{% endif %} - {% if not instance.is_deleted and instance.spec and spec_fieldsets %} + {% if instance.spec and spec_fieldsets %}
diff --git a/src/servala/frontend/templates/frontend/organizations/service_instances.html b/src/servala/frontend/templates/frontend/organizations/service_instances.html index e5c7c0e..c443e6e 100644 --- a/src/servala/frontend/templates/frontend/organizations/service_instances.html +++ b/src/servala/frontend/templates/frontend/organizations/service_instances.html @@ -27,7 +27,6 @@ {% translate "Service Provider" %} {% translate "Service Provider Zone" %} {% translate "Created At" %} - {% translate "Status" %} @@ -40,17 +39,10 @@ {{ instance.context.service_offering.provider.name }} {{ instance.context.control_plane.name }} {{ instance.created_at|date:"SHORT_DATETIME_FORMAT" }} - - {% if instance.is_deleted %} - {% translate "Deleted" %} - {% else %} - {% translate "Active" %} - {% endif %} - {% empty %} - {% translate "No service instances found." %} + {% translate "No service instances found." %} {% endfor %} diff --git a/src/servala/frontend/views/generic.py b/src/servala/frontend/views/generic.py index 16b1ec2..170b159 100644 --- a/src/servala/frontend/views/generic.py +++ b/src/servala/frontend/views/generic.py @@ -1,6 +1,6 @@ from django.conf import settings from django.contrib.auth.mixins import LoginRequiredMixin -from django.db.models import Count, Q +from django.db.models import Count from django.shortcuts import redirect, render from django.urls import reverse_lazy from django.utils.functional import cached_property @@ -32,9 +32,7 @@ class OrganizationSelectionView(LoginRequiredMixin, TemplateView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["user_organizations"] = self.user_organizations.annotate( - instance_count=Count( - "service_instances", filter=Q(service_instances__is_deleted=False) - ) + instance_count=Count("service_instances") ) return context diff --git a/src/servala/frontend/views/organization.py b/src/servala/frontend/views/organization.py index 6545d39..2f35f76 100644 --- a/src/servala/frontend/views/organization.py +++ b/src/servala/frontend/views/organization.py @@ -70,9 +70,7 @@ class OrganizationDashboardView( context = super().get_context_data(**kwargs) organization = self.get_object() - service_instances = ServiceInstance.objects.filter( - organization=organization, is_deleted=False - ) + service_instances = ServiceInstance.objects.filter(organization=organization) recent_instances = service_instances.order_by("-created_at")[:5] for instance in recent_instances: diff --git a/src/servala/frontend/views/service.py b/src/servala/frontend/views/service.py index f9ce50d..201a510 100644 --- a/src/servala/frontend/views/service.py +++ b/src/servala/frontend/views/service.py @@ -198,11 +198,7 @@ class ServiceInstanceMixin: def get_object(self, **kwargs): instance = super().get_object(**kwargs) - if ( - not instance.is_deleted - and not instance.kubernetes_object - and not self._has_warned - ): + if not instance.kubernetes_object and not self._has_warned: messages.warning( self.request, _( @@ -221,11 +217,7 @@ class ServiceInstanceDetailView( def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - if ( - not self.object.is_deleted - and self.object.kubernetes_object - and self.object.spec - ): + if self.object.kubernetes_object and self.object.spec: context["spec_fieldsets"] = self.get_nested_spec() context["has_change_permission"] = self.request.user.has_perm( ServiceInstance.get_perm("change"), self.object @@ -406,15 +398,6 @@ class ServiceInstanceListView(OrganizationViewMixin, ListView): ) if self.filter_form.is_valid(): queryset = self.filter_form.filter_queryset(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): @@ -433,7 +416,7 @@ class ServiceInstanceDeleteView( def form_valid(self, form): try: - self.object.delete_instance(user=self.request.user) + self.object.delete(user=self.request.user) messages.success( self.request, _("Service instance '{name}' has been scheduled for deletion.").format( diff --git a/src/servala/settings.py b/src/servala/settings.py index 63f7895..81cdc9c 100644 --- a/src/servala/settings.py +++ b/src/servala/settings.py @@ -157,6 +157,7 @@ INSTALLED_APPS = [ "allauth.account", "allauth.socialaccount", "allauth.socialaccount.providers.openid_connect", + "auditlog", "servala.core", ] @@ -170,6 +171,7 @@ MIDDLEWARE = [ "django.middleware.clickjacking.XFrameOptionsMiddleware", "allauth.account.middleware.AccountMiddleware", "django.contrib.auth.middleware.LoginRequiredMiddleware", + "auditlog.middleware.AuditlogMiddleware", "servala.core.middleware.OrganizationMiddleware", ] LOGIN_URL = "account_login" diff --git a/uv.lock b/uv.lock index 675f389..86fd355 100644 --- a/uv.lock +++ b/uv.lock @@ -376,6 +376,19 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/ac/82/e6f607b0bad524d227f6e5aaffdb5e2b286f6ab1b4b3151134ae2303c2d6/django_allauth-65.11.1.tar.gz", hash = "sha256:e95d5234cccaf92273d315e1393cc4626cb88a19d66a1bf0e81f89f7958cfa06", size = 1915592, upload-time = "2025-08-27T18:05:05.581Z" } +[[package]] +name = "django-auditlog" +version = "3.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/46/9da1d94493832fa18d2f6324a76d387fa232001593866987a96047709f4e/django_auditlog-3.2.1.tar.gz", hash = "sha256:63a4c9f7793e94eed804bc31a04d9b0b58244b1d280e2ed273c8b406bff1f779", size = 72926, upload-time = "2025-07-03T20:08:17.734Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/06/67296d050a72dcd76f57f220df621cb27e5b9282ba7ad0f5f74870dce241/django_auditlog-3.2.1-py3-none-any.whl", hash = "sha256:99603ca9d015f7e9b062b1c34f3e0826a3ce6ae6e5950c81bb7e663f7802a899", size = 38330, upload-time = "2025-07-03T20:07:51.735Z" }, +] + [[package]] name = "django-fernet-encrypted-fields" version = "0.3.0" @@ -1129,6 +1142,7 @@ dependencies = [ { name = "cryptography" }, { name = "django" }, { name = "django-allauth" }, + { name = "django-auditlog" }, { name = "django-fernet-encrypted-fields" }, { name = "django-jsonform" }, { name = "django-scopes" }, @@ -1166,6 +1180,7 @@ requires-dist = [ { name = "cryptography", specifier = ">=45.0.7" }, { name = "django", specifier = "==5.2.6" }, { name = "django-allauth", specifier = ">=65.10.0" }, + { name = "django-auditlog", specifier = ">=3.2.1" }, { name = "django-fernet-encrypted-fields", specifier = ">=0.3.0" }, { name = "django-jsonform", specifier = ">=2.23.2" }, { name = "django-scopes", specifier = ">=2.0.0" },