from __future__ import annotations import uuid from dataclasses import dataclass import auto_prefetch from auto_prefetch import ForeignKey from auto_prefetch import Manager from django.core.validators import MaxValueValidator from django.core.validators import MinValueValidator from django.db import models from django.db import transaction from control_plane.runtime_plans import DjangoApplicationLaunchConfig from control_plane.runtime_plans import build_django_server_command class TimestampedModel(auto_prefetch.Model): """Provide created and updated timestamps for control-plane records.""" created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta(auto_prefetch.Model.Meta): abstract = True class DeploymentStatus(models.TextChoices): """Track deployment lifecycle state inside control plane.""" QUEUED = "queued", "Queued" PROVISIONING = "provisioning", "Provisioning" BOOTING = "booting", "Booting" RUNNING = "running", "Running" FAILED = "failed", "Failed" STOPPED = "stopped", "Stopped" DESTROYING = "destroying", "Destroying" DESTROYED = "destroyed", "Destroyed" class RuntimeServiceKind(models.TextChoices): """Enumerate deployment-scoped backing services.""" POSTGRESQL = "postgresql", "PostgreSQL" REDIS = "redis", "Redis" class RuntimeServiceStatus(models.TextChoices): """Track lifecycle state for a deployment-scoped service.""" QUEUED = "queued", "Queued" PROVISIONING = "provisioning", "Provisioning" READY = "ready", "Ready" FAILED = "failed", "Failed" DESTROYING = "destroying", "Destroying" DESTROYED = "destroyed", "Destroyed" @dataclass(frozen=True, slots=True) class RuntimeServiceSeedSpec: """Describe default values for admin-seeded test runtime services.""" hostname: str image_reference: str internal_port: int RUNTIME_SERVICE_SEED_SPECS: dict[RuntimeServiceKind, RuntimeServiceSeedSpec] = { RuntimeServiceKind.POSTGRESQL: RuntimeServiceSeedSpec( hostname="postgres.internal", image_reference="docker.io/library/postgres:17-alpine", internal_port=5432, ), RuntimeServiceKind.REDIS: RuntimeServiceSeedSpec( hostname="redis.internal", image_reference="docker.io/library/redis:7.4-alpine", internal_port=6379, ), } def _build_limited_identifier( *, prefix: str, tenant_slug: str, site_slug: str, suffix: str, max_length: int, ) -> str: """Build a bounded identifier while preserving deployment uniqueness. Args: prefix: Static prefix to identify the type of resource (e.g. "net" or "postgres"). tenant_slug: Hosted site tenant slug to include in the name for uniqueness. site_slug: Hosted site slug to include in the name for uniqueness. suffix: Unique suffix to ensure no collisions across deployments of the same site. max_length: Maximum length for the resulting identifier. Returns: A string that combines the prefix, tenant slug, site slug, and suffix, truncated as needed to fit within max_length. """ candidate = f"{prefix}-{tenant_slug}-{site_slug}-{suffix}" if len(candidate) <= max_length: return candidate min_length = len(prefix) + len(suffix) + 2 if min_length >= max_length: return f"{prefix}-{suffix}"[:max_length] remaining_length = max_length - len(prefix) - len(suffix) - 3 tenant_budget = max(1, remaining_length // 2) site_budget = max(1, remaining_length - tenant_budget) return "-".join( ( prefix, tenant_slug[:tenant_budget], site_slug[:site_budget], suffix, ), ) def _build_limited_connection_name(*, site_slug: str, suffix: str, max_length: int = 63) -> str: """Build a bounded database identifier that stays unique per deployment. Args: site_slug: Hosted site slug to include in the name for uniqueness. suffix: Unique suffix to ensure no collisions across deployments of the same site. max_length: Maximum length for the resulting identifier, defaulting to 63 for database compatibility Returns: A string that combines the site slug and suffix, truncated as needed to fit within max_length. """ candidate = f"{site_slug}-{suffix}" if len(candidate) <= max_length: return candidate min_length = len(suffix) + 1 if min_length >= max_length: return suffix[:max_length] site_budget = max_length - len(suffix) - 1 return f"{site_slug[:site_budget]}-{suffix}" class Tenant(TimestampedModel): """Represent a tenant that owns hosted applications and deployments.""" id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) slug = models.SlugField(max_length=64, unique=True) display_name = models.CharField(max_length=255) objects = Manager() class Meta(TimestampedModel.Meta): ordering = ("slug",) indexes = [models.Index(fields=("slug",), name="tenant_slug_idx")] def __str__(self) -> str: return self.display_name class HostedSite(TimestampedModel): """Describe a deployable Django site owned by a tenant.""" id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) tenant = ForeignKey(Tenant, on_delete=models.CASCADE, related_name="hosted_sites") slug = models.SlugField(max_length=64) display_name = models.CharField(max_length=255) working_directory = models.CharField(max_length=255, default=".") wsgi_module = models.CharField(max_length=255) service_port = models.PositiveIntegerField( default=8000, validators=[MinValueValidator(1024), MaxValueValidator(65535)], ) objects = Manager() class Meta(TimestampedModel.Meta): ordering = ("tenant__slug", "slug") constraints = [ models.UniqueConstraint( fields=("tenant", "slug"), name="hosted_site_unique_tenant_slug", ), ] indexes = [ models.Index(fields=("tenant", "slug"), name="site_tenant_slug_idx"), ] def __str__(self) -> str: return f"{self.tenant.slug}/{self.slug}" class Deployment(TimestampedModel): """Track a single deployable runtime instance for a hosted site.""" id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) hosted_site = ForeignKey(HostedSite, on_delete=models.CASCADE, related_name="deployments") idempotency_key = models.CharField(max_length=64, unique=True) source_sha256 = models.CharField(max_length=64) status = models.CharField( max_length=32, choices=DeploymentStatus, default=DeploymentStatus.QUEUED, ) guest_ipv4 = models.GenericIPAddressField(protocol="IPv4", blank=True, null=True) guest_port = models.PositiveIntegerField( default=8000, validators=[MinValueValidator(1024), MaxValueValidator(65535)], ) firecracker_vm_id = models.CharField(max_length=64, blank=True, null=True, unique=True) last_error = models.TextField(blank=True) started_at = models.DateTimeField(blank=True, null=True) finished_at = models.DateTimeField(blank=True, null=True) objects = Manager() class Meta(TimestampedModel.Meta): ordering = ("-created_at",) indexes = [ models.Index(fields=("hosted_site", "status"), name="deploy_site_status_idx"), models.Index(fields=("status", "created_at"), name="deploy_status_created_idx"), ] def __str__(self) -> str: return f"{self.hosted_site} [{self.status}]" def build_django_launch_command(self) -> tuple[str, ...]: """Build a uv-driven Gunicorn command for this deployment's Django app. Returns: Tuple of command arguments ready for subprocess execution inside a guest VM. """ config = DjangoApplicationLaunchConfig( wsgi_module=self.hosted_site.wsgi_module, bind_host="0.0.0.0", # noqa: S104 port=self.guest_port, ) return build_django_server_command(config) def ensure_test_runtime_services(self) -> tuple[RuntimeService, ...]: """Create missing test runtime services for all supported service kinds. Returns: Newly created runtime service records. """ tenant_slug = self.hosted_site.tenant.slug site_slug = self.hosted_site.slug deployment_suffix = self.id.hex[:12] network_name = _build_limited_identifier( prefix="net", tenant_slug=tenant_slug, site_slug=site_slug, suffix=deployment_suffix, max_length=128, ) connection_name = _build_limited_connection_name( site_slug=site_slug, suffix=deployment_suffix, ) created_services: list[RuntimeService] = [] with transaction.atomic(): existing_kinds = set( RuntimeService.objects.filter(deployment=self).values_list("kind", flat=True), ) for kind, seed_spec in RUNTIME_SERVICE_SEED_SPECS.items(): if kind.value in existing_kinds: continue created_services.append( RuntimeService( deployment=self, kind=kind.value, status=RuntimeServiceStatus.QUEUED.value, container_name=_build_limited_identifier( prefix=kind.value, tenant_slug=tenant_slug, site_slug=site_slug, suffix=deployment_suffix, max_length=128, ), network_name=network_name, hostname=seed_spec.hostname, image_reference=seed_spec.image_reference, internal_port=seed_spec.internal_port, connection_username=connection_name if kind == RuntimeServiceKind.POSTGRESQL else "", connection_database=connection_name if kind == RuntimeServiceKind.POSTGRESQL else "", connection_secret_ref=(f"secret://{kind.value}/{tenant_slug}/{site_slug}/{deployment_suffix}"), ), ) if created_services: RuntimeService.objects.bulk_create(created_services) return tuple(created_services) class RuntimeService(TimestampedModel): """Track a dedicated PostgreSQL or Redis service for one deployment.""" id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) deployment = ForeignKey(Deployment, on_delete=models.CASCADE, related_name="runtime_services") kind = models.CharField(max_length=32, choices=RuntimeServiceKind) status = models.CharField( max_length=32, choices=RuntimeServiceStatus, default=RuntimeServiceStatus.QUEUED, ) container_name = models.CharField(max_length=128, unique=True) network_name = models.CharField(max_length=128) hostname = models.CharField(max_length=128) image_reference = models.CharField(max_length=255) internal_port = models.PositiveIntegerField( validators=[MinValueValidator(1), MaxValueValidator(65535)], ) connection_username = models.CharField(max_length=63, blank=True) connection_database = models.CharField(max_length=63, blank=True) connection_secret_ref = models.CharField(max_length=255) objects = Manager() class Meta(TimestampedModel.Meta): ordering = ("deployment__created_at", "kind") constraints = [ models.UniqueConstraint( fields=("deployment", "kind"), name="runtime_service_unique_deployment_kind", ), ] indexes = [ models.Index(fields=("deployment", "kind"), name="service_deploy_kind_idx"), models.Index(fields=("kind", "status"), name="service_kind_status_idx"), ] def __str__(self) -> str: return f"{self.deployment_id}:{self.kind}"