Compare commits

...

1 Commits

Author SHA1 Message Date
zhi
c1288b5fa9 feat: wizard config volume integration
- entrypoint.sh: wait for config file before starting uvicorn
- config.py: resolve DB URL from wizard config volume
- init_wizard.py: read config from file instead of HTTP
- Dockerfile: use entrypoint.sh
2026-03-06 13:46:38 +00:00
4 changed files with 95 additions and 41 deletions

View File

@@ -15,9 +15,10 @@ RUN pip install --no-cache-dir -r requirements.txt
# Copy application code # Copy application code
COPY . . COPY . .
RUN chmod +x entrypoint.sh
# Expose port # Expose port
EXPOSE 8000 EXPOSE 8000
# Run the application # Wait for wizard config, then start uvicorn
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] ENTRYPOINT ["./entrypoint.sh"]

View File

@@ -1,3 +1,5 @@
import os
import json
from sqlalchemy import create_engine from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import sessionmaker
@@ -5,6 +7,30 @@ from pydantic_settings import BaseSettings
from typing import Optional 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): class Settings(BaseSettings):
DATABASE_URL: str = "mysql+pymysql://harborforge:harborforge_pass@mysql:3306/harborforge" DATABASE_URL: str = "mysql+pymysql://harborforge:harborforge_pass@mysql:3306/harborforge"
SECRET_KEY: str = "change-me-in-production" SECRET_KEY: str = "change-me-in-production"
@@ -18,7 +44,9 @@ class Settings(BaseSettings):
settings = Settings() 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) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base() Base = declarative_base()

View File

@@ -1,13 +1,12 @@
""" """
HarborForge initialization via AbstractWizard. HarborForge initialization from AbstractWizard config volume.
On startup, reads config from AbstractWizard and creates: Reads config from shared volume (written by AbstractWizard).
- Admin user (if not exists) On startup, creates admin user and default project if not exists.
- Default project (if configured)
""" """
import os import os
import json
import logging import logging
import httpx
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.models import models from app.models import models
@@ -15,40 +14,45 @@ from app.api.deps import get_password_hash
logger = logging.getLogger("harborforge.init") logger = logging.getLogger("harborforge.init")
WIZARD_URL = os.getenv("WIZARD_URL", "http://wizard:8080") CONFIG_DIR = os.getenv("CONFIG_DIR", "/config")
WIZARD_CONFIG = os.getenv("WIZARD_CONFIG", "harborforge.json") CONFIG_FILE = os.getenv("CONFIG_FILE", "harborforge.json")
def fetch_wizard_config() -> dict | None: def load_config() -> dict | None:
"""Fetch initialization config from AbstractWizard.""" """Load initialization config from shared volume."""
url = f"{WIZARD_URL}/api/v1/config/{WIZARD_CONFIG}" 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: try:
resp = httpx.get(url, timeout=10) with open(config_path, "r") as f:
if resp.status_code == 200: return json.load(f)
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
except Exception as e: 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 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.""" """Create admin user if not exists."""
username = admin_cfg.get("username", "admin") username = admin_cfg.get("username", "admin")
existing = db.query(models.User).filter(models.User.username == username).first() existing = db.query(models.User).filter(models.User.username == username).first()
if existing: if existing:
logger.info("Admin user '%s' already exists (id=%d), skipping", username, existing.id) logger.info("Admin user '%s' already exists (id=%d), skipping", username, existing.id)
return return existing
password = admin_cfg.get("password", "changeme") password = admin_cfg.get("password", "changeme")
user = models.User( user = models.User(
@@ -63,11 +67,14 @@ def init_admin_user(db: Session, admin_cfg: dict) -> None:
db.commit() db.commit()
db.refresh(user) db.refresh(user)
logger.info("Created admin user '%s' (id=%d)", username, user.id) 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.""" """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() existing = db.query(models.Project).filter(models.Project.name == name).first()
if existing: if existing:
logger.info("Project '%s' already exists (id=%d), skipping", name, existing.id) 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( project = models.Project(
name=name, name=name,
description=project_cfg.get("description", ""), description=project_cfg.get("description", ""),
owner_id=admin_user_id, owner_id=owner_id,
) )
db.add(project) db.add(project)
db.commit() 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: def run_init(db: Session) -> None:
"""Main initialization entry point.""" """Main initialization entry point. Reads config from shared volume."""
config = fetch_wizard_config() config = load_config()
if not config: if not config:
return return
logger.info("Running HarborForge initialization from AbstractWizard") logger.info("Running HarborForge initialization from wizard config")
# Admin user # Admin user
admin_cfg = config.get("admin") admin_cfg = config.get("admin")
admin_user = None
if admin_cfg: if admin_cfg:
init_admin_user(db, admin_cfg) admin_user = init_admin_user(db, admin_cfg)
# Default project # Default project
project_cfg = config.get("default_project") project_cfg = config.get("default_project")
if project_cfg: if project_cfg and admin_user:
admin = db.query(models.User).filter(models.User.is_admin == True).first() init_default_project(db, project_cfg, admin_user.id)
if admin:
init_default_project(db, project_cfg, admin.id)
logger.info("Initialization complete") logger.info("Initialization complete")

19
entrypoint.sh Normal file
View File

@@ -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