Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .envs/.ci/.django
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,8 @@ RABBITMQ_DEFAULT_PASS=rabbitpass
# NATS
# ------------------------------------------------------------------------------
NATS_URL=nats://nats:4222

# Enable idempotent bootstrap for CI so processing services can self-register.
DJANGO_SUPERUSER_EMAIL=antenna@insectai.org
DJANGO_SUPERUSER_PASSWORD=localadmin
ENSURE_DEFAULT_PROJECT=1
4 changes: 4 additions & 0 deletions .envs/.local/.django
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,7 @@ MINIO_BROWSER_REDIRECT_URL=http://minio:9001
DEFAULT_PROCESSING_SERVICE_NAME=Local Processing Service
DEFAULT_PROCESSING_SERVICE_ENDPOINT=http://ml_backend:2000
# DEFAULT_PIPELINES_ENABLED=random,constant # When set to None, all pipelines will be enabled.

# Idempotent local/CI bootstrap (ami/main/management/commands/ensure_default_project.py)
# Ensures a default superuser + project exist so processing services can self-register.
ENSURE_DEFAULT_PROJECT=1
77 changes: 77 additions & 0 deletions ami/main/management/commands/ensure_default_project.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
"""
Idempotent bootstrap command for local dev and CI.

Ensures a superuser and a named Project exist so that local-dev / CI processing
services can register pipelines against Antenna without any manual setup step.

Guarded behind the ENSURE_DEFAULT_PROJECT env var so production deployments
never run it accidentally. Intended to be called from compose/local/django/start.

Looks up / creates the Project by name (no slug field on Project) so running
this in a long-lived dev DB where PK 1 is already taken by a different project
doesn't conflict.
"""

import logging
import os

from django.contrib.auth import get_user_model
from django.core.management import call_command
from django.core.management.base import BaseCommand
from django.db import transaction

from ami.main.models import Project

logger = logging.getLogger(__name__)

DEFAULT_PROJECT_NAME = "Default Project"


class Command(BaseCommand):
help = "Idempotently create a default superuser and project for local dev / CI."

def add_arguments(self, parser):
parser.add_argument(
"--project-name",
default=os.environ.get("ANTENNA_DEFAULT_PROJECT_NAME", DEFAULT_PROJECT_NAME),
help="Project name to ensure exists (default: env ANTENNA_DEFAULT_PROJECT_NAME or 'Default Project')",
)

def handle(self, *args, **options):
User = get_user_model()

try:
call_command("createsuperuser", interactive=False)
self.stdout.write(self.style.SUCCESS("Created superuser from DJANGO_SUPERUSER_* env vars"))
except Exception as e:
# createsuperuser raises CommandError if the user already exists;
# that's the idempotent path we want.
logger.info("Superuser createsuperuser call reported: %s", e)

email = os.environ.get("DJANGO_SUPERUSER_EMAIL")
owner = User.objects.filter(email=email).first() if email else None
if owner is None:
self.stdout.write(
self.style.WARNING(
"No DJANGO_SUPERUSER_EMAIL env var (or user not found). "
"Project will be created without an owner."
)
)

project_name = options["project_name"]
with transaction.atomic():
project, created = Project.objects.get_or_create(
name=project_name,
defaults={"owner": owner, "description": "Bootstrap project for local dev and CI."},
)

if created:
self.stdout.write(self.style.SUCCESS(f"Created project '{project_name}' (id={project.pk})"))
else:
self.stdout.write(f"Project '{project_name}' already exists (id={project.pk})")

# Print in a stable, parseable format so shell wrappers can capture the
# ID. Compose files can't read command output — they use env vars — so
# the PS container reads ANTENNA_DEFAULT_PROJECT_NAME and resolves
# to a PK via the REST API rather than relying on PK being stable.
self.stdout.write(f"ANTENNA_DEFAULT_PROJECT_ID={project.pk}")
8 changes: 8 additions & 0 deletions compose/local/django/start
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ set -o nounset

python manage.py migrate

# Idempotent bootstrap for local dev and CI. Creates the default superuser
# (from DJANGO_SUPERUSER_* env vars) and a named project so processing-service
# containers can self-register against Antenna with no manual setup.
# Safe to run in production: gated behind ENSURE_DEFAULT_PROJECT=1.
if [ "${ENSURE_DEFAULT_PROJECT:-0}" = "1" ]; then
python manage.py ensure_default_project || echo "ensure_default_project failed, continuing"
fi

# Set USE_UVICORN=1 to use the original raw uvicorn dev server instead of gunicorn
if [ "${USE_UVICORN:-0}" = "1" ]; then
if [ "${DEBUGGER:-0}" = "1" ]; then
Expand Down
Loading
Loading