Bootstrap initial admin via env and add compose profiles
This commit is contained in:
@@ -12,6 +12,7 @@
|
|||||||
# @SIDE_EFFECT: Starts background scheduler and binds network ports for HTTP/WS traffic.
|
# @SIDE_EFFECT: Starts background scheduler and binds network ports for HTTP/WS traffic.
|
||||||
# @DATA_CONTRACT: [HTTP Request | WS Message] -> [HTTP Response | JSON Log Stream]
|
# @DATA_CONTRACT: [HTTP Request | WS Message] -> [HTTP Response | JSON Log Stream]
|
||||||
|
|
||||||
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
# project_root is used for static files mounting
|
# project_root is used for static files mounting
|
||||||
@@ -28,6 +29,9 @@ from .dependencies import get_task_manager, get_scheduler_service
|
|||||||
from .core.encryption_key import ensure_encryption_key
|
from .core.encryption_key import ensure_encryption_key
|
||||||
from .core.utils.network import NetworkError
|
from .core.utils.network import NetworkError
|
||||||
from .core.logger import logger, belief_scope
|
from .core.logger import logger, belief_scope
|
||||||
|
from .core.database import AuthSessionLocal
|
||||||
|
from .core.auth.security import get_password_hash
|
||||||
|
from .models.auth import User, Role
|
||||||
from .api.routes import plugins, tasks, settings, environments, mappings, migration, connections, git, storage, admin, llm, dashboards, datasets, reports, assistant, clean_release, clean_release_v2, profile, health, dataset_review
|
from .api.routes import plugins, tasks, settings, environments, mappings, migration, connections, git, storage, admin, llm, dashboards, datasets, reports, assistant, clean_release, clean_release_v2, profile, health, dataset_review
|
||||||
from .api import auth
|
from .api import auth
|
||||||
|
|
||||||
@@ -42,6 +46,54 @@ app = FastAPI(
|
|||||||
)
|
)
|
||||||
# [/DEF:App:Global]
|
# [/DEF:App:Global]
|
||||||
|
|
||||||
|
# [DEF:ensure_initial_admin_user:Function]
|
||||||
|
# @COMPLEXITY: 3
|
||||||
|
# @PURPOSE: Ensures initial admin user exists when bootstrap env flags are enabled.
|
||||||
|
def ensure_initial_admin_user() -> None:
|
||||||
|
raw_flag = os.getenv("INITIAL_ADMIN_CREATE", "false").strip().lower()
|
||||||
|
if raw_flag not in {"1", "true", "yes", "on"}:
|
||||||
|
return
|
||||||
|
username = os.getenv("INITIAL_ADMIN_USERNAME", "").strip()
|
||||||
|
password = os.getenv("INITIAL_ADMIN_PASSWORD", "").strip()
|
||||||
|
if not username or not password:
|
||||||
|
logger.warning(
|
||||||
|
"INITIAL_ADMIN_CREATE is enabled but INITIAL_ADMIN_USERNAME/INITIAL_ADMIN_PASSWORD is missing; skipping bootstrap."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
db = AuthSessionLocal()
|
||||||
|
try:
|
||||||
|
admin_role = db.query(Role).filter(Role.name == "Admin").first()
|
||||||
|
if not admin_role:
|
||||||
|
admin_role = Role(name="Admin", description="System Administrator")
|
||||||
|
db.add(admin_role)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(admin_role)
|
||||||
|
|
||||||
|
existing_user = db.query(User).filter(User.username == username).first()
|
||||||
|
if existing_user:
|
||||||
|
logger.info("Initial admin bootstrap skipped: user '%s' already exists.", username)
|
||||||
|
return
|
||||||
|
|
||||||
|
new_user = User(
|
||||||
|
username=username,
|
||||||
|
email=None,
|
||||||
|
password_hash=get_password_hash(password),
|
||||||
|
auth_source="LOCAL",
|
||||||
|
is_active=True,
|
||||||
|
)
|
||||||
|
new_user.roles.append(admin_role)
|
||||||
|
db.add(new_user)
|
||||||
|
db.commit()
|
||||||
|
logger.info("Initial admin user '%s' created from environment bootstrap.", username)
|
||||||
|
except Exception as exc:
|
||||||
|
db.rollback()
|
||||||
|
logger.error("Failed to bootstrap initial admin user: %s", exc)
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
# [/DEF:ensure_initial_admin_user:Function]
|
||||||
|
|
||||||
# [DEF:startup_event:Function]
|
# [DEF:startup_event:Function]
|
||||||
# @COMPLEXITY: 3
|
# @COMPLEXITY: 3
|
||||||
# @PURPOSE: Handles application startup tasks, such as starting the scheduler.
|
# @PURPOSE: Handles application startup tasks, such as starting the scheduler.
|
||||||
@@ -53,6 +105,7 @@ app = FastAPI(
|
|||||||
async def startup_event():
|
async def startup_event():
|
||||||
with belief_scope("startup_event"):
|
with belief_scope("startup_event"):
|
||||||
ensure_encryption_key()
|
ensure_encryption_key()
|
||||||
|
ensure_initial_admin_user()
|
||||||
scheduler = get_scheduler_service()
|
scheduler = get_scheduler_service()
|
||||||
scheduler.start()
|
scheduler.start()
|
||||||
# [/DEF:startup_event:Function]
|
# [/DEF:startup_event:Function]
|
||||||
|
|||||||
35
build.sh
35
build.sh
@@ -7,6 +7,23 @@ cd "$SCRIPT_DIR"
|
|||||||
|
|
||||||
BACKEND_ENV_FILE="$SCRIPT_DIR/backend/.env"
|
BACKEND_ENV_FILE="$SCRIPT_DIR/backend/.env"
|
||||||
|
|
||||||
|
PROFILE="${1:-current}"
|
||||||
|
|
||||||
|
case "$PROFILE" in
|
||||||
|
master)
|
||||||
|
PROFILE_ENV_FILE="$SCRIPT_DIR/.env.master"
|
||||||
|
PROJECT_NAME="ss-tools-master"
|
||||||
|
;;
|
||||||
|
current)
|
||||||
|
PROFILE_ENV_FILE="$SCRIPT_DIR/.env.current"
|
||||||
|
PROJECT_NAME="ss-tools-current"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Error: unknown profile '$PROFILE'. Use one of: master, current."
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
if ! command -v docker >/dev/null 2>&1; then
|
if ! command -v docker >/dev/null 2>&1; then
|
||||||
echo "Error: docker is not installed or not in PATH."
|
echo "Error: docker is not installed or not in PATH."
|
||||||
exit 1
|
exit 1
|
||||||
@@ -80,11 +97,23 @@ PY
|
|||||||
|
|
||||||
ensure_backend_encryption_key
|
ensure_backend_encryption_key
|
||||||
|
|
||||||
|
COMPOSE_ARGS=(-p "$PROJECT_NAME")
|
||||||
|
if [[ -f "$PROFILE_ENV_FILE" ]]; then
|
||||||
|
COMPOSE_ARGS+=(--env-file "$PROFILE_ENV_FILE")
|
||||||
|
else
|
||||||
|
echo "[build] Warning: profile env file not found at $PROFILE_ENV_FILE, using compose defaults."
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[build] Profile: $PROFILE (project: $PROJECT_NAME)"
|
||||||
|
if [[ -f "$PROFILE_ENV_FILE" ]]; then
|
||||||
|
echo "[build] Env file: $PROFILE_ENV_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
echo "[1/2] Building project images..."
|
echo "[1/2] Building project images..."
|
||||||
"${COMPOSE_CMD[@]}" build
|
"${COMPOSE_CMD[@]}" "${COMPOSE_ARGS[@]}" build
|
||||||
|
|
||||||
echo "[2/2] Starting Docker services..."
|
echo "[2/2] Starting Docker services..."
|
||||||
"${COMPOSE_CMD[@]}" up -d
|
"${COMPOSE_CMD[@]}" "${COMPOSE_ARGS[@]}" up -d
|
||||||
|
|
||||||
echo "Done. Services are running."
|
echo "Done. Services are running."
|
||||||
echo "Use '${COMPOSE_CMD[*]} ps' to check status and '${COMPOSE_CMD[*]} logs -f' to stream logs."
|
echo "Use '${COMPOSE_CMD[*]} ${COMPOSE_ARGS[*]} ps' to check status and '${COMPOSE_CMD[*]} ${COMPOSE_ARGS[*]} logs -f' to stream logs."
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ services:
|
|||||||
db:
|
db:
|
||||||
image: ${POSTGRES_IMAGE:?Set POSTGRES_IMAGE in .env.enterprise-clean}
|
image: ${POSTGRES_IMAGE:?Set POSTGRES_IMAGE in .env.enterprise-clean}
|
||||||
pull_policy: never
|
pull_policy: never
|
||||||
container_name: ss_tools_db
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_DB: ${POSTGRES_DB:-ss_tools}
|
POSTGRES_DB: ${POSTGRES_DB:-ss_tools}
|
||||||
@@ -21,7 +20,6 @@ services:
|
|||||||
backend:
|
backend:
|
||||||
image: ${BACKEND_IMAGE:?Set BACKEND_IMAGE in .env.enterprise-clean}
|
image: ${BACKEND_IMAGE:?Set BACKEND_IMAGE in .env.enterprise-clean}
|
||||||
pull_policy: never
|
pull_policy: never
|
||||||
container_name: ss_tools_backend
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
depends_on:
|
depends_on:
|
||||||
db:
|
db:
|
||||||
@@ -50,7 +48,6 @@ services:
|
|||||||
frontend:
|
frontend:
|
||||||
image: ${FRONTEND_IMAGE:?Set FRONTEND_IMAGE in .env.enterprise-clean}
|
image: ${FRONTEND_IMAGE:?Set FRONTEND_IMAGE in .env.enterprise-clean}
|
||||||
pull_policy: never
|
pull_policy: never
|
||||||
container_name: ss_tools_frontend
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
depends_on:
|
depends_on:
|
||||||
- backend
|
- backend
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
db:
|
db:
|
||||||
image: ${POSTGRES_IMAGE:-postgres:16-alpine}
|
image: ${POSTGRES_IMAGE:-postgres:16-alpine}
|
||||||
container_name: ss_tools_db
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_DB: ss_tools
|
POSTGRES_DB: ss_tools
|
||||||
@@ -21,7 +20,6 @@ services:
|
|||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: docker/backend.Dockerfile
|
dockerfile: docker/backend.Dockerfile
|
||||||
container_name: ss_tools_backend
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
env_file:
|
env_file:
|
||||||
- ./backend/.env
|
- ./backend/.env
|
||||||
@@ -34,6 +32,9 @@ services:
|
|||||||
TASKS_DATABASE_URL: postgresql+psycopg2://postgres:postgres@db:5432/ss_tools
|
TASKS_DATABASE_URL: postgresql+psycopg2://postgres:postgres@db:5432/ss_tools
|
||||||
AUTH_DATABASE_URL: postgresql+psycopg2://postgres:postgres@db:5432/ss_tools
|
AUTH_DATABASE_URL: postgresql+psycopg2://postgres:postgres@db:5432/ss_tools
|
||||||
BACKEND_PORT: 8000
|
BACKEND_PORT: 8000
|
||||||
|
INITIAL_ADMIN_CREATE: ${INITIAL_ADMIN_CREATE:-false}
|
||||||
|
INITIAL_ADMIN_USERNAME: ${INITIAL_ADMIN_USERNAME:-admin}
|
||||||
|
INITIAL_ADMIN_PASSWORD: ${INITIAL_ADMIN_PASSWORD:-}
|
||||||
ports:
|
ports:
|
||||||
- "${BACKEND_HOST_PORT:-8001}:8000"
|
- "${BACKEND_HOST_PORT:-8001}:8000"
|
||||||
volumes:
|
volumes:
|
||||||
@@ -46,7 +47,6 @@ services:
|
|||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: docker/frontend.Dockerfile
|
dockerfile: docker/frontend.Dockerfile
|
||||||
container_name: ss_tools_frontend
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
depends_on:
|
depends_on:
|
||||||
- backend
|
- backend
|
||||||
|
|||||||
Reference in New Issue
Block a user