Skip to content
Draft
Show file tree
Hide file tree
Changes from 2 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