diff --git a/Dockerfile b/Dockerfile index d782d96..ec2b7f9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,9 +15,10 @@ RUN pip install --no-cache-dir -r requirements.txt # Copy application code COPY . . +RUN chmod +x entrypoint.sh # Expose port EXPOSE 8000 -# Run the application -CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] +# Wait for wizard config, then start uvicorn +ENTRYPOINT ["./entrypoint.sh"] diff --git a/app/core/config.py b/app/core/config.py index 09cd622..8200002 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -1,3 +1,5 @@ +import os +import json from sqlalchemy import create_engine from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker @@ -5,6 +7,30 @@ from pydantic_settings import BaseSettings from typing import Optional +def _resolve_db_url(env_url: str) -> str: + """Read DB config from wizard config volume if available, else use env.""" + config_dir = os.getenv("CONFIG_DIR", "/config") + config_file = os.getenv("CONFIG_FILE", "harborforge.json") + config_path = os.path.join(config_dir, config_file) + + if os.path.exists(config_path): + try: + with open(config_path, "r") as f: + cfg = json.load(f) + db_cfg = cfg.get("database") + if db_cfg: + host = db_cfg.get("host", "mysql") + port = db_cfg.get("port", 3306) + user = db_cfg.get("user", "harborforge") + password = db_cfg.get("password", "harborforge_pass") + database = db_cfg.get("database", "harborforge") + return f"mysql+pymysql://{user}:{password}@{host}:{port}/{database}" + except Exception: + pass + + return env_url + + class Settings(BaseSettings): DATABASE_URL: str = "mysql+pymysql://harborforge:harborforge_pass@mysql:3306/harborforge" SECRET_KEY: str = "change-me-in-production" @@ -18,7 +44,9 @@ class Settings(BaseSettings): settings = Settings() -engine = create_engine(settings.DATABASE_URL, pool_pre_ping=True) +# Resolve DB URL: wizard config volume > env > default +_db_url = _resolve_db_url(settings.DATABASE_URL) +engine = create_engine(_db_url, pool_pre_ping=True) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) Base = declarative_base() diff --git a/app/init_wizard.py b/app/init_wizard.py index 6d385db..85b9e4d 100644 --- a/app/init_wizard.py +++ b/app/init_wizard.py @@ -1,13 +1,12 @@ """ -HarborForge initialization via AbstractWizard. +HarborForge initialization from AbstractWizard config volume. -On startup, reads config from AbstractWizard and creates: -- Admin user (if not exists) -- Default project (if configured) +Reads config from shared volume (written by AbstractWizard). +On startup, creates admin user and default project if not exists. """ import os +import json import logging -import httpx from sqlalchemy.orm import Session from app.models import models @@ -15,40 +14,45 @@ from app.api.deps import get_password_hash logger = logging.getLogger("harborforge.init") -WIZARD_URL = os.getenv("WIZARD_URL", "http://wizard:8080") -WIZARD_CONFIG = os.getenv("WIZARD_CONFIG", "harborforge.json") +CONFIG_DIR = os.getenv("CONFIG_DIR", "/config") +CONFIG_FILE = os.getenv("CONFIG_FILE", "harborforge.json") -def fetch_wizard_config() -> dict | None: - """Fetch initialization config from AbstractWizard.""" - url = f"{WIZARD_URL}/api/v1/config/{WIZARD_CONFIG}" +def load_config() -> dict | None: + """Load initialization config from shared volume.""" + config_path = os.path.join(CONFIG_DIR, CONFIG_FILE) + if not os.path.exists(config_path): + logger.info("No config file at %s, skipping initialization", config_path) + return None try: - resp = httpx.get(url, timeout=10) - if resp.status_code == 200: - data = resp.json() - # AbstractWizard wraps in {"data": ...} - return data.get("data", data) - elif resp.status_code == 404: - logger.info("No wizard config found at %s, skipping initialization", url) - return None - else: - logger.warning("Wizard returned %d: %s", resp.status_code, resp.text) - return None - except httpx.ConnectError: - logger.info("AbstractWizard not available at %s, skipping", WIZARD_URL) - return None + with open(config_path, "r") as f: + return json.load(f) except Exception as e: - logger.warning("Failed to fetch wizard config: %s", e) + logger.warning("Failed to read config %s: %s", config_path, e) return None -def init_admin_user(db: Session, admin_cfg: dict) -> None: +def get_db_url(config: dict) -> str | None: + """Build DATABASE_URL from wizard config, or fall back to env.""" + db_cfg = config.get("database") + if not db_cfg: + return os.getenv("DATABASE_URL") + + host = db_cfg.get("host", "mysql") + port = db_cfg.get("port", 3306) + user = db_cfg.get("user", "harborforge") + password = db_cfg.get("password", "harborforge_pass") + database = db_cfg.get("database", "harborforge") + return f"mysql+pymysql://{user}:{password}@{host}:{port}/{database}" + + +def init_admin_user(db: Session, admin_cfg: dict) -> models.User | None: """Create admin user if not exists.""" username = admin_cfg.get("username", "admin") existing = db.query(models.User).filter(models.User.username == username).first() if existing: logger.info("Admin user '%s' already exists (id=%d), skipping", username, existing.id) - return + return existing password = admin_cfg.get("password", "changeme") user = models.User( @@ -63,11 +67,14 @@ def init_admin_user(db: Session, admin_cfg: dict) -> None: db.commit() db.refresh(user) logger.info("Created admin user '%s' (id=%d)", username, user.id) + return user -def init_default_project(db: Session, project_cfg: dict, admin_user_id: int) -> None: +def init_default_project(db: Session, project_cfg: dict, owner_id: int) -> None: """Create default project if configured and not exists.""" - name = project_cfg.get("name", "Default") + name = project_cfg.get("name") + if not name: + return existing = db.query(models.Project).filter(models.Project.name == name).first() if existing: logger.info("Project '%s' already exists (id=%d), skipping", name, existing.id) @@ -76,7 +83,7 @@ def init_default_project(db: Session, project_cfg: dict, admin_user_id: int) -> project = models.Project( name=name, description=project_cfg.get("description", ""), - owner_id=admin_user_id, + owner_id=owner_id, ) db.add(project) db.commit() @@ -85,23 +92,22 @@ def init_default_project(db: Session, project_cfg: dict, admin_user_id: int) -> def run_init(db: Session) -> None: - """Main initialization entry point.""" - config = fetch_wizard_config() + """Main initialization entry point. Reads config from shared volume.""" + config = load_config() if not config: return - logger.info("Running HarborForge initialization from AbstractWizard") + logger.info("Running HarborForge initialization from wizard config") # Admin user admin_cfg = config.get("admin") + admin_user = None if admin_cfg: - init_admin_user(db, admin_cfg) + admin_user = init_admin_user(db, admin_cfg) # Default project project_cfg = config.get("default_project") - if project_cfg: - admin = db.query(models.User).filter(models.User.is_admin == True).first() - if admin: - init_default_project(db, project_cfg, admin.id) + if project_cfg and admin_user: + init_default_project(db, project_cfg, admin_user.id) logger.info("Initialization complete") diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000..0a318fb --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,19 @@ +#!/bin/sh +# Wait for wizard config before starting uvicorn +CONFIG_DIR="${CONFIG_DIR:-/config}" +CONFIG_FILE="${CONFIG_FILE:-harborforge.json}" +CONFIG_PATH="$CONFIG_DIR/$CONFIG_FILE" + +echo "HarborForge Backend - waiting for config..." +echo " Config path: $CONFIG_PATH" + +while true; do + if [ -f "$CONFIG_PATH" ]; then + echo " Config found! Starting backend..." + break + fi + echo " Config not ready, waiting 5s... (run setup wizard via SSH tunnel)" + sleep 5 +done + +exec uvicorn app.main:app --host 0.0.0.0 --port 8000