This commit is contained in:
Joakim Hellsén 2026-04-27 20:43:26 +02:00
commit a7a5b5c8ea
Signed by: Joakim Hellsén
SSH key fingerprint: SHA256:/9h/CsExpFp+PRhsfA0xznFx2CGfTT5R/kpuFfUgEQk
43 changed files with 5531 additions and 9 deletions

223
control_plane/admin.py Normal file
View file

@ -0,0 +1,223 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from django.contrib import admin
from django.contrib import messages
from django.db.models import Count
from django.db.models import F
from control_plane.models import Deployment
from control_plane.models import HostedSite
from control_plane.models import RuntimeService
from control_plane.models import RuntimeServiceKind
from control_plane.models import Tenant
from control_plane.tasks import provision_test_runtime_services
if TYPE_CHECKING:
from django.db.models import QuerySet
from django.http import HttpRequest
RuntimeServiceInlineBase = admin.StackedInline[RuntimeService]
TenantAdminBase = admin.ModelAdmin[Tenant]
HostedSiteAdminBase = admin.ModelAdmin[HostedSite]
DeploymentAdminBase = admin.ModelAdmin[Deployment]
RuntimeServiceAdminBase = admin.ModelAdmin[RuntimeService]
else:
RuntimeServiceInlineBase = admin.StackedInline
TenantAdminBase = admin.ModelAdmin
HostedSiteAdminBase = admin.ModelAdmin
DeploymentAdminBase = admin.ModelAdmin
RuntimeServiceAdminBase = admin.ModelAdmin
class RuntimeServiceInline(RuntimeServiceInlineBase):
"""Allow deployment admins to create/edit related runtime services inline."""
model = RuntimeService
extra = 0
max_num = len(RuntimeServiceKind)
show_change_link = True
@admin.register(Tenant)
class TenantAdmin(TenantAdminBase):
"""Expose tenants for admin-managed smoke data setup."""
list_display = ("slug", "display_name")
search_fields = ("slug", "display_name")
ordering = ("slug",)
@admin.register(HostedSite)
class HostedSiteAdmin(HostedSiteAdminBase):
"""Expose hosted sites so admins can build deployment test graphs."""
list_display = ("slug", "display_name", "tenant_slug", "service_port")
list_filter = ("tenant",)
search_fields = (
"slug",
"display_name",
"tenant__slug",
"tenant__display_name",
"wsgi_module",
)
ordering = ("tenant__slug", "slug")
autocomplete_fields = ("tenant",)
list_select_related = ("tenant",)
def get_queryset(self, request: HttpRequest) -> QuerySet[HostedSite]:
"""Load tenant slug values for changelist rendering.
Returns:
Hosted site queryset with tenant join and tenant slug annotation.
"""
return (
super()
.get_queryset(request)
.select_related("tenant")
.annotate(
tenant_slug_value=F("tenant__slug"),
)
)
@admin.display(ordering="tenant__slug", description="Tenant")
def tenant_slug(self, hosted_site: HostedSite) -> str:
"""Return tenant slug for changelist display and sorting."""
return str(vars(hosted_site)["tenant_slug_value"])
@admin.register(Deployment)
class DeploymentAdmin(DeploymentAdminBase):
"""Expose deployments and queue test container provisioning."""
list_display = (
"id",
"status",
"tenant_slug",
"site_slug",
"idempotency_key",
"guest_port",
"runtime_service_total",
)
list_filter = ("status",)
search_fields = (
"=id",
"idempotency_key",
"firecracker_vm_id",
"hosted_site__slug",
"hosted_site__tenant__slug",
)
ordering = ("hosted_site__tenant__slug", "hosted_site__slug", "-created_at")
autocomplete_fields = ("hosted_site",)
list_select_related = ("hosted_site__tenant",)
inlines = (RuntimeServiceInline,)
actions = ("create_test_containers",)
def get_queryset(self, request: HttpRequest) -> QuerySet[Deployment]:
"""Load related hosted site and tenant rows for admin rendering.
Returns:
Deployment queryset with hosted site and tenant joined.
"""
return (
super()
.get_queryset(request)
.select_related("hosted_site__tenant")
.annotate(
tenant_slug_value=F("hosted_site__tenant__slug"),
site_slug_value=F("hosted_site__slug"),
runtime_service_total_value=Count("runtime_services", distinct=True),
)
)
@admin.display(ordering="hosted_site__tenant__slug", description="Tenant")
def tenant_slug(self, deployment: Deployment) -> str:
"""Return tenant slug for changelist display and sorting."""
return str(vars(deployment)["tenant_slug_value"])
@admin.display(ordering="hosted_site__slug", description="Site")
def site_slug(self, deployment: Deployment) -> str:
"""Return hosted site slug for changelist display and sorting."""
return str(vars(deployment)["site_slug_value"])
@admin.display(description="Runtime services")
def runtime_service_total(self, deployment: Deployment) -> int:
"""Return total runtime services currently linked to a deployment."""
return int(vars(deployment)["runtime_service_total_value"])
@admin.action(description="Queue test container provisioning")
def create_test_containers(
self,
request: HttpRequest,
queryset: QuerySet[Deployment],
) -> None:
"""Queue Celery jobs that seed and provision local test containers."""
deployment_ids = [str(deployment_id) for deployment_id in queryset.values_list("id", flat=True)]
for deployment_id in deployment_ids:
provision_test_runtime_services.delay(deployment_id)
self.message_user(
request,
(
f"Queued test container provisioning for {len(deployment_ids)} deployments. "
"Run a Celery worker to execute queued jobs."
),
level=messages.SUCCESS,
)
@admin.register(RuntimeService)
class RuntimeServiceAdmin(RuntimeServiceAdminBase):
"""Expose runtime service containers to Django admin users."""
list_display = (
"container_name",
"kind",
"status",
"tenant_slug",
"site_slug",
"internal_port",
)
list_filter = ("kind", "status")
search_fields = (
"container_name",
"network_name",
"hostname",
"deployment__idempotency_key",
"deployment__hosted_site__slug",
"deployment__hosted_site__tenant__slug",
)
ordering = (
"deployment__hosted_site__tenant__slug",
"deployment__hosted_site__slug",
"kind",
)
autocomplete_fields = ("deployment",)
list_select_related = ("deployment__hosted_site__tenant",)
def get_queryset(self, request: HttpRequest) -> QuerySet[RuntimeService]:
"""Load related deployment context for changelist rendering.
Returns:
Runtime service queryset with deployment, site, and tenant joined.
"""
return (
super()
.get_queryset(request)
.select_related("deployment__hosted_site__tenant")
.annotate(
tenant_slug_value=F("deployment__hosted_site__tenant__slug"),
site_slug_value=F("deployment__hosted_site__slug"),
)
)
@admin.display(ordering="deployment__hosted_site__tenant__slug", description="Tenant")
def tenant_slug(self, runtime_service: RuntimeService) -> str:
"""Return tenant slug for changelist display and sorting."""
return str(vars(runtime_service)["tenant_slug_value"])
@admin.display(ordering="deployment__hosted_site__slug", description="Site")
def site_slug(self, runtime_service: RuntimeService) -> str:
"""Return hosted site slug for changelist display and sorting."""
return str(vars(runtime_service)["site_slug_value"])