256 lines
11 KiB
Python
256 lines
11 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import sys
|
|
from typing import Any
|
|
|
|
from fastapi import Body, Depends, FastAPI, HTTPException, Request
|
|
from fastapi.responses import JSONResponse, Response
|
|
from prometheus_client import CONTENT_TYPE_LATEST, generate_latest
|
|
|
|
from .app_account_routes import _register_account_routes
|
|
from .app_admin_routes import _register_admin_routes
|
|
from .auth.keycloak import AuthContext, authenticator
|
|
from .db.database import Database, DatabaseConfig
|
|
from .db.storage import Storage
|
|
from .manager.provisioning import ProvisioningManager
|
|
from .metrics.metrics import record_task_run
|
|
from .scheduler.cron import CronScheduler
|
|
from .services.cluster_state import run_cluster_state
|
|
from .services.comms import comms
|
|
from .services.firefly import firefly
|
|
from .services.image_sweeper import image_sweeper
|
|
from .services.jenkins_build_weather import collect_jenkins_build_weather
|
|
from .services.jenkins_workspace_cleanup import cleanup_jenkins_workspace_storage
|
|
from .services.keycloak_admin import keycloak_admin
|
|
from .services.keycloak_profile import run_profile_sync
|
|
from .services.mailu import mailu
|
|
from .services.mailu_events import mailu_events
|
|
from .services.metis import metis
|
|
from .services.metis_token_sync import metis_token_sync
|
|
from .services.nextcloud import nextcloud
|
|
from .services.opensearch_prune import prune_indices
|
|
from .services.platform_quality_probe import platform_quality_probe
|
|
from .services.pod_cleaner import clean_finished_pods
|
|
from .services.vault import vault
|
|
from .services.vaultwarden_sync import run_vaultwarden_sync
|
|
from .services.wger import wger
|
|
from .settings import settings
|
|
from .utils.http import extract_bearer_token
|
|
from .utils.logging import LogConfig, configure_logging, get_logger
|
|
from .utils.passwords import random_password
|
|
|
|
|
|
configure_logging(LogConfig(level=settings.log_level))
|
|
logger = get_logger(__name__)
|
|
|
|
portal_db = Database(
|
|
settings.portal_database_url,
|
|
DatabaseConfig(
|
|
pool_min=settings.ariadne_db_pool_min,
|
|
pool_max=settings.ariadne_db_pool_max,
|
|
connect_timeout_sec=settings.ariadne_db_connect_timeout_sec,
|
|
lock_timeout_sec=settings.ariadne_db_lock_timeout_sec,
|
|
statement_timeout_sec=settings.ariadne_db_statement_timeout_sec,
|
|
idle_in_tx_timeout_sec=settings.ariadne_db_idle_in_tx_timeout_sec,
|
|
application_name="ariadne_portal",
|
|
),
|
|
)
|
|
ariadne_db = Database(
|
|
settings.ariadne_database_url,
|
|
DatabaseConfig(
|
|
pool_min=settings.ariadne_db_pool_min,
|
|
pool_max=settings.ariadne_db_pool_max,
|
|
connect_timeout_sec=settings.ariadne_db_connect_timeout_sec,
|
|
lock_timeout_sec=settings.ariadne_db_lock_timeout_sec,
|
|
statement_timeout_sec=settings.ariadne_db_statement_timeout_sec,
|
|
idle_in_tx_timeout_sec=settings.ariadne_db_idle_in_tx_timeout_sec,
|
|
application_name="ariadne",
|
|
),
|
|
)
|
|
storage = Storage(ariadne_db, portal_db)
|
|
provisioning = ProvisioningManager(portal_db, storage)
|
|
scheduler = CronScheduler(storage, settings.schedule_tick_sec)
|
|
app = FastAPI(title=settings.app_name)
|
|
|
|
|
|
def _record_event(event_type: str, detail: dict[str, Any] | str | None) -> None:
|
|
try:
|
|
storage.record_event(event_type, detail)
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
def _parse_event_detail(detail: str | None) -> Any:
|
|
if not isinstance(detail, str) or not detail:
|
|
return ""
|
|
try:
|
|
return json.loads(detail)
|
|
except Exception:
|
|
return detail
|
|
|
|
|
|
def _require_auth(request: Request) -> AuthContext:
|
|
token = extract_bearer_token(request)
|
|
if not token:
|
|
raise HTTPException(status_code=401, detail="missing bearer token")
|
|
try:
|
|
return authenticator.authenticate(token)
|
|
except Exception:
|
|
raise HTTPException(status_code=401, detail="invalid token")
|
|
|
|
|
|
def _require_admin(ctx: AuthContext) -> None:
|
|
if ctx.username and ctx.username in settings.portal_admin_users:
|
|
return
|
|
if settings.portal_admin_groups and set(ctx.groups).intersection(settings.portal_admin_groups):
|
|
return
|
|
raise HTTPException(status_code=403, detail="forbidden")
|
|
|
|
|
|
def _require_account_access(ctx: AuthContext) -> None:
|
|
if not settings.account_allowed_groups:
|
|
return
|
|
if not ctx.groups:
|
|
return
|
|
if set(ctx.groups).intersection(settings.account_allowed_groups):
|
|
return
|
|
raise HTTPException(status_code=403, detail="forbidden")
|
|
|
|
|
|
async def _read_json_payload(request: Request) -> dict[str, Any]:
|
|
try:
|
|
payload = await request.json()
|
|
except Exception:
|
|
return {}
|
|
return payload if isinstance(payload, dict) else {}
|
|
|
|
|
|
def _note_from_payload(payload: dict[str, Any]) -> str | None:
|
|
note = payload.get("note") if isinstance(payload, dict) else None
|
|
return str(note).strip() if isinstance(note, str) and note.strip() else None
|
|
|
|
|
|
def _flags_from_payload(payload: dict[str, Any]) -> list[str]:
|
|
flags_raw = payload.get("flags") if isinstance(payload, dict) else None
|
|
return [flag for flag in flags_raw if isinstance(flag, str)] if isinstance(flags_raw, list) else []
|
|
|
|
|
|
def _allowed_flag_groups() -> list[str]:
|
|
if not keycloak_admin.ready():
|
|
return settings.allowed_flag_groups
|
|
try:
|
|
return keycloak_admin.list_group_names(exclude={"admin"})
|
|
except Exception:
|
|
return settings.allowed_flag_groups
|
|
|
|
|
|
def _app_module() -> Any:
|
|
return sys.modules[__name__]
|
|
|
|
|
|
@app.on_event("startup")
|
|
def _startup() -> None:
|
|
provisioning.start()
|
|
|
|
scheduler.add_task("schedule.mailu_sync", settings.mailu_sync_cron, lambda: mailu.sync("ariadne_schedule"))
|
|
scheduler.add_task("schedule.nextcloud_sync", settings.nextcloud_sync_cron, lambda: nextcloud.sync_mail(wait=False))
|
|
scheduler.add_task("schedule.nextcloud_cron", settings.nextcloud_cron, lambda: nextcloud.run_cron())
|
|
scheduler.add_task("schedule.nextcloud_maintenance", settings.nextcloud_maintenance_cron, lambda: nextcloud.run_maintenance())
|
|
scheduler.add_task("schedule.vaultwarden_sync", settings.vaultwarden_sync_cron, run_vaultwarden_sync)
|
|
scheduler.add_task("schedule.keycloak_profile", settings.keycloak_profile_cron, run_profile_sync)
|
|
scheduler.add_task("schedule.wger_user_sync", settings.wger_user_sync_cron, lambda: wger.sync_users())
|
|
scheduler.add_task("schedule.wger_admin", settings.wger_admin_cron, lambda: wger.ensure_admin(wait=False))
|
|
scheduler.add_task("schedule.firefly_user_sync", settings.firefly_user_sync_cron, lambda: firefly.sync_users())
|
|
scheduler.add_task("schedule.firefly_cron", settings.firefly_cron, lambda: firefly.run_cron())
|
|
scheduler.add_task("schedule.pod_cleaner", settings.pod_cleaner_cron, clean_finished_pods)
|
|
scheduler.add_task("schedule.opensearch_prune", settings.opensearch_prune_cron, prune_indices)
|
|
scheduler.add_task("schedule.image_sweeper", settings.image_sweeper_cron, lambda: image_sweeper.run(wait=True))
|
|
scheduler.add_task("schedule.metis_sentinel_watch", settings.metis_sentinel_watch_cron, lambda: metis.watch_sentinel())
|
|
scheduler.add_task("schedule.metis_k3s_token_sync", settings.metis_k3s_token_sync_cron, lambda: metis_token_sync.run(wait=True))
|
|
scheduler.add_task(
|
|
"schedule.platform_quality_suite_probe",
|
|
settings.platform_quality_suite_probe_cron,
|
|
lambda: platform_quality_probe.run(wait=True),
|
|
)
|
|
scheduler.add_task("schedule.jenkins_build_weather", settings.jenkins_build_weather_cron, collect_jenkins_build_weather)
|
|
scheduler.add_task("schedule.jenkins_workspace_cleanup", settings.jenkins_workspace_cleanup_cron, cleanup_jenkins_workspace_storage)
|
|
scheduler.add_task("schedule.vault_k8s_auth", settings.vault_k8s_auth_cron, lambda: vault.sync_k8s_auth(wait=True))
|
|
scheduler.add_task("schedule.vault_oidc", settings.vault_oidc_cron, lambda: vault.sync_oidc(wait=True))
|
|
scheduler.add_task("schedule.comms_guest_name", settings.comms_guest_name_cron, lambda: comms.run_guest_name_randomizer(wait=True))
|
|
scheduler.add_task("schedule.comms_pin_invite", settings.comms_pin_invite_cron, lambda: comms.run_pin_invite(wait=True))
|
|
scheduler.add_task("schedule.comms_reset_room", settings.comms_reset_room_cron, lambda: comms.run_reset_room(wait=True))
|
|
scheduler.add_task("schedule.comms_seed_room", settings.comms_seed_room_cron, lambda: comms.run_seed_room(wait=True))
|
|
scheduler.add_task("schedule.cluster_state", settings.cluster_state_cron, lambda: run_cluster_state(storage))
|
|
scheduler.start()
|
|
logger.info(
|
|
"ariadne started",
|
|
extra={
|
|
"event": "startup",
|
|
"mailu_cron": settings.mailu_sync_cron,
|
|
"nextcloud_mail_cron": settings.nextcloud_sync_cron,
|
|
"nextcloud_cron": settings.nextcloud_cron,
|
|
"nextcloud_maintenance_cron": settings.nextcloud_maintenance_cron,
|
|
"vaultwarden_cron": settings.vaultwarden_sync_cron,
|
|
"wger_user_sync_cron": settings.wger_user_sync_cron,
|
|
"wger_admin_cron": settings.wger_admin_cron,
|
|
"firefly_user_sync_cron": settings.firefly_user_sync_cron,
|
|
"firefly_cron": settings.firefly_cron,
|
|
"pod_cleaner_cron": settings.pod_cleaner_cron,
|
|
"opensearch_prune_cron": settings.opensearch_prune_cron,
|
|
"image_sweeper_cron": settings.image_sweeper_cron,
|
|
"metis_sentinel_watch_cron": settings.metis_sentinel_watch_cron,
|
|
"metis_k3s_token_sync_cron": settings.metis_k3s_token_sync_cron,
|
|
"platform_quality_suite_probe_cron": settings.platform_quality_suite_probe_cron,
|
|
"jenkins_build_weather_cron": settings.jenkins_build_weather_cron,
|
|
"jenkins_base_url": settings.jenkins_base_url,
|
|
"jenkins_workspace_cleanup_cron": settings.jenkins_workspace_cleanup_cron,
|
|
"jenkins_workspace_cleanup_dry_run": settings.jenkins_workspace_cleanup_dry_run,
|
|
"jenkins_workspace_cleanup_max_deletions_per_run": settings.jenkins_workspace_cleanup_max_deletions_per_run,
|
|
"vault_k8s_auth_cron": settings.vault_k8s_auth_cron,
|
|
"vault_oidc_cron": settings.vault_oidc_cron,
|
|
"comms_guest_name_cron": settings.comms_guest_name_cron,
|
|
"comms_pin_invite_cron": settings.comms_pin_invite_cron,
|
|
"comms_reset_room_cron": settings.comms_reset_room_cron,
|
|
"comms_seed_room_cron": settings.comms_seed_room_cron,
|
|
"keycloak_profile_cron": settings.keycloak_profile_cron,
|
|
"cluster_state_cron": settings.cluster_state_cron,
|
|
},
|
|
)
|
|
|
|
|
|
@app.on_event("shutdown")
|
|
def _shutdown() -> None:
|
|
scheduler.stop()
|
|
provisioning.stop()
|
|
portal_db.close()
|
|
ariadne_db.close()
|
|
logger.info("ariadne stopped", extra={"event": "shutdown"})
|
|
|
|
|
|
@app.get("/health")
|
|
def health() -> dict[str, Any]:
|
|
"""Return a minimal liveness response for probes and operators."""
|
|
|
|
return {"ok": True}
|
|
|
|
|
|
@app.get(settings.metrics_path)
|
|
def metrics() -> Response:
|
|
"""Expose Prometheus metrics generated by Ariadne runtime tasks."""
|
|
|
|
payload = generate_latest()
|
|
return Response(payload, media_type=CONTENT_TYPE_LATEST)
|
|
|
|
|
|
@app.post("/events")
|
|
def mailu_event_listener(payload: dict[str, Any] | None = Body(default=None)) -> Response:
|
|
"""Accept Mailu webhook events and dispatch mapped account actions."""
|
|
|
|
status_code, response = mailu_events.handle_event(payload)
|
|
return JSONResponse(response, status_code=status_code)
|
|
|
|
|
|
_register_admin_routes(app, _require_auth, _app_module)
|
|
_register_account_routes(app, _require_auth, _app_module)
|