feat(backend)!: kill AbstractWizard, env-driven config + hf-cli
Drops the AbstractWizard config-volume bootstrap entirely. All deploy-time
config now comes from docker env vars (.env). First-deploy admin user + OIDC
provider config are operator-driven via `docker exec hf_backend hf-cli ...`.
Backend changes:
- entrypoint.sh: drop config-wait loop, just exec uvicorn
- app/core/config.py: drop _resolve_db_url + OIDC_* env vars (DB only now);
keep HARBORFORGE_OIDC_ONLY (deploy-time policy)
- app/init_wizard.py → app/init_bootstrap.py: drop load_config / admin / OIDC /
default-project bootstrap; keep idempotent startup seed (permissions,
default roles, acc-mgr + deleted-user builtins)
- app/main.py: /config/status now returns {initialized: <admin exists>};
startup() imports init_bootstrap.run_bootstrap
- app/api/routers/oidc.py: get_effective_oidc reads DB only (no env fallback)
- app/services/harborforge_config.py: removed (replaced by direct env reads)
- app/services/discord_wakeup.py: HF_DISCORD_GUILD_ID / HF_DISCORD_BOT_TOKEN env
- app/api/routers/users.py + tests/conftest.py: rename init_wizard refs
New hf-cli surface (app/cli/, invoked via /usr/local/bin/hf-cli shim):
hf-cli admin create-user --email <e> [--username <u>] [--password <p>]
[--oidc-issuer <url> --oidc-subject <sub>]
hf-cli admin list
hf-cli admin set-role --username <u> --role <admin|mgr|dev|guest|account-manager>
hf-cli admin reset-password --username <u> --password <p>
hf-cli admin bind-oidc --username <u> --oidc-issuer <url> --oidc-subject <sub>
hf-cli config oidc [--issuer/...] [--client-id/...] [--client-secret/...]
[--redirect-uri/...] [--enabled true|false] [--show-secret]
Bootstrap migration on existing deployments: existing admin / OIDC settings
in the DB are preserved across the cutover; only the wizard config-volume
+ wizard sidecar services need to be removed from compose. Restart picks
up the new entrypoint + skips the config wait.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,34 +1,13 @@
|
||||
import os
|
||||
import json
|
||||
"""Backend runtime settings — env-only (no wizard / no config volume).
|
||||
|
||||
OIDC issuer/client_id/etc. live in the `oidc_settings` DB table set
|
||||
via `hf-cli config oidc ...`. The OIDC_ONLY flag remains env-driven
|
||||
because it's a deploy-time policy, not a per-tenant runtime config.
|
||||
"""
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
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):
|
||||
@@ -38,19 +17,9 @@ class Settings(BaseSettings):
|
||||
ALGORITHM: str = "HS256"
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
|
||||
|
||||
# --- OIDC (generic, OpenID Connect discovery) ---
|
||||
OIDC_ENABLED: bool = False
|
||||
OIDC_ISSUER: str = "" # e.g. https://idp.example.com (we use {issuer}/.well-known/openid-configuration)
|
||||
OIDC_CLIENT_ID: str = ""
|
||||
OIDC_CLIENT_SECRET: str = ""
|
||||
OIDC_REDIRECT_URI: str = "" # backend callback, e.g. https://hf-api.example.com/auth/oidc/callback
|
||||
OIDC_SCOPES: str = "openid email profile"
|
||||
OIDC_POST_LOGIN_REDIRECT: str = "" # frontend URL to return to (token in fragment). Falls back to "/"
|
||||
OIDC_ADMIN_ROLE: str = "admin" # OIDC role name that bootstraps the unbound hf admin (OIDC-only)
|
||||
|
||||
# When true: no password login at all. Password login endpoint rejects,
|
||||
# user creation ignores any password (passwordless user that can only use
|
||||
# API keys / OIDC), and the frontend hides all password UI.
|
||||
# user creation ignores any password (passwordless users that only sign
|
||||
# in via a bound OIDC identity / API keys), frontend hides password UI.
|
||||
HARBORFORGE_OIDC_ONLY: bool = False
|
||||
|
||||
class Config:
|
||||
@@ -75,9 +44,7 @@ if settings.SECRET_KEY in _WEAK_SECRETS or len(settings.SECRET_KEY) < 32:
|
||||
"Refusing to start with a default/short key."
|
||||
)
|
||||
|
||||
# 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)
|
||||
engine = create_engine(settings.DATABASE_URL, pool_pre_ping=True)
|
||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
Base = declarative_base()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user