game-stream: align Ariadne Wolf support with current app

This commit is contained in:
codex 2026-05-21 02:29:20 -03:00
parent 2687ac441e
commit dc0fccbbc6
26 changed files with 716 additions and 3779 deletions

File diff suppressed because it is too large Load Diff

168
ariadne/app_game_routes.py Normal file
View File

@ -0,0 +1,168 @@
from __future__ import annotations
from datetime import datetime, timezone
import secrets
from typing import Any, Callable
from fastapi import Depends, FastAPI, HTTPException, Request
from fastapi.responses import JSONResponse
from .auth.keycloak import AuthContext
from .db.storage import TaskRunRecord
from .utils.errors import safe_error_detail
def _game_from_payload(payload: dict[str, Any]) -> str:
game = payload.get("game") if isinstance(payload, dict) else None
return str(game).strip() if isinstance(game, str) and game.strip() else "wolf"
def _record_simple_task(module: Any, task_name: str, started: datetime, status: str, detail: str | None = None) -> None:
finished = datetime.now(timezone.utc)
duration_sec = (finished - started).total_seconds()
module.record_task_run(task_name, status, duration_sec)
try:
module.storage.record_task_run(
TaskRunRecord(
request_code=None,
task=task_name,
status=status,
detail=detail,
started_at=started,
finished_at=finished,
duration_ms=int(duration_sec * 1000),
)
)
except Exception:
pass
def _require_game_mode_hook(module: Any, request: Request) -> None:
expected = module.settings.game_mode_hook_token
if not expected:
raise HTTPException(status_code=503, detail="game mode hook token not configured")
token = request.headers.get("x-ariadne-game-mode-token", "")
if not token and request.headers.get("authorization", "").lower().startswith("bearer "):
token = request.headers.get("authorization", "")[7:].strip()
if not secrets.compare_digest(token, expected):
raise HTTPException(status_code=401, detail="invalid game mode hook token")
async def _run_game_mode_action(module: Any, action: str, payload: dict[str, Any], actor: str) -> JSONResponse:
started = datetime.now(timezone.utc)
status = "ok"
error_detail = ""
game = _game_from_payload(payload)
note = module._note_from_payload(payload)
task_name = f"game_mode_{action}"
try:
if action == "start":
result = module.game_mode.start(game, note=note)
elif action == "stop":
result = module.game_mode.stop(game, note=note)
else:
raise HTTPException(status_code=400, detail="invalid action")
module._record_event(task_name, {"actor": actor, "status": "ok", "game": game, "note": note or "", "result": result})
return JSONResponse(result)
except HTTPException:
status = "error"
raise
except Exception as exc:
status = "error"
error_detail = safe_error_detail(exc, f"game mode {action} failed")
module._record_event(task_name, {"actor": actor, "status": "error", "game": game, "error": error_detail})
raise HTTPException(status_code=502, detail=error_detail)
finally:
_record_simple_task(module, task_name, started, status, error_detail or None)
def _ensure_wolf_oauth2(module: Any, ctx: AuthContext) -> JSONResponse:
module._require_admin(ctx)
started = datetime.now(timezone.utc)
status = "ok"
detail = ""
try:
result = module.oauth2_proxy.ensure_wolf()
if result.get("status") != "ok":
status = "error"
detail = str(result.get("detail") or "wolf oauth2 ensure failed")
raise HTTPException(status_code=502, detail=detail)
module._record_event("wolf_oidc_ensure", {"actor": ctx.username or "admin", **result})
return JSONResponse(result)
except HTTPException:
raise
except Exception as exc:
status = "error"
detail = safe_error_detail(exc, "wolf oauth2 ensure failed")
module._record_event("wolf_oidc_ensure", {"actor": ctx.username or "admin", "status": "error", "detail": detail})
raise HTTPException(status_code=502, detail=detail)
finally:
_record_simple_task(module, "wolf_oidc_ensure", started, status, detail or None)
def _register_game_routes(app: FastAPI, require_auth: Callable, deps: Callable[[], Any]) -> None:
@app.get("/api/game-stream/me")
def get_game_stream_profile(ctx: AuthContext = Depends(require_auth)) -> JSONResponse:
"""Return the Wolf profile policy for the authenticated Keycloak user."""
module = deps()
return JSONResponse(module.game_stream_profiles.profile_for(ctx.username or "", ctx.groups))
@app.get("/api/admin/game-mode/status")
def get_game_mode_status(ctx: AuthContext = Depends(require_auth)) -> JSONResponse:
"""Return the current game-mode state for authenticated administrators."""
module = deps()
module._require_admin(ctx)
try:
return JSONResponse(module.game_mode.status())
except Exception:
raise HTTPException(status_code=502, detail="failed to load game mode status")
@app.post("/api/admin/game-mode/start")
async def start_game_mode(request: Request, ctx: AuthContext = Depends(require_auth)) -> JSONResponse:
"""Scale infrastructure GPU workloads down for an administrator-triggered game session."""
module = deps()
module._require_admin(ctx)
payload = await module._read_json_payload(request)
return await _run_game_mode_action(module, "start", payload, ctx.username or "admin")
@app.post("/api/admin/game-mode/stop")
async def stop_game_mode(request: Request, ctx: AuthContext = Depends(require_auth)) -> JSONResponse:
"""Restore infrastructure GPU workloads after an administrator-triggered game session."""
module = deps()
module._require_admin(ctx)
payload = await module._read_json_payload(request)
return await _run_game_mode_action(module, "stop", payload, ctx.username or "admin")
@app.post("/api/game-mode/start")
async def start_game_mode_hook(request: Request) -> JSONResponse:
"""Scale infrastructure GPU workloads down for a trusted game-stream hook."""
module = deps()
_require_game_mode_hook(module, request)
payload = await module._read_json_payload(request)
return await _run_game_mode_action(module, "start", payload, "game-stream-hook")
@app.post("/api/game-mode/stop")
async def stop_game_mode_hook(request: Request) -> JSONResponse:
"""Restore infrastructure GPU workloads for a trusted game-stream hook."""
module = deps()
_require_game_mode_hook(module, request)
payload = await module._read_json_payload(request)
return await _run_game_mode_action(module, "stop", payload, "game-stream-hook")
@app.post("/api/admin/game-stream/wolf/oauth2/ensure")
def ensure_wolf_oauth2(ctx: AuthContext = Depends(require_auth)) -> JSONResponse:
"""Ensure Keycloak and Vault state for the Wolf oauth2-proxy."""
return _ensure_wolf_oauth2(deps(), ctx)
@app.post("/api/admin/game-stream/sunshine/oauth2/ensure")
def ensure_sunshine_oauth2_alias(ctx: AuthContext = Depends(require_auth)) -> JSONResponse:
"""Keep the old Sunshine route as a transition alias for Wolf."""
return _ensure_wolf_oauth2(deps(), ctx)

View File

@ -24,12 +24,7 @@ def _read_service_account() -> tuple[str, str]:
return token, str(ca_path)
def _k8s_request(
method: str,
path: str,
payload: dict[str, Any] | None = None,
extra_headers: dict[str, str] | None = None,
) -> Any:
def _k8s_request(method: str, path: str, payload: dict[str, Any] | None = None, extra_headers: dict[str, str] | None = None) -> Any:
token, ca_path = _read_service_account()
url = f"{_K8S_BASE_URL}{path}"
headers = {"Authorization": f"Bearer {token}"}
@ -62,12 +57,7 @@ def post_json(path: str, payload: dict[str, Any]) -> dict[str, Any]:
def patch_json(path: str, payload: dict[str, Any]) -> dict[str, Any]:
"""Patch a Kubernetes API resource with a JSON merge patch."""
data = _k8s_request(
"PATCH",
path,
payload,
{"Content-Type": "application/merge-patch+json"},
)
data = _k8s_request("PATCH", path, payload, {"Content-Type": "application/merge-patch+json"})
if not isinstance(data, dict):
raise RuntimeError("unexpected kubernetes response")
return data

View File

@ -99,13 +99,7 @@ def record_task_run(task: str, status: str, duration_sec: float | None) -> None:
TASK_DURATION_SECONDS.labels(task=task, status=status).observe(duration_sec)
def record_schedule_state(
task: str,
last_run_ts: float | None,
last_success_ts: float | None,
next_run_ts: float | None,
ok: bool | None,
) -> None:
def record_schedule_state(task: str, last_run_ts: float | None, last_success_ts: float | None, next_run_ts: float | None, ok: bool | None) -> None:
"""Publish the latest scheduler timestamps and status for a task."""
if last_run_ts:
@ -127,13 +121,7 @@ def set_access_request_counts(counts: dict[str, int]) -> None:
ACCESS_REQUESTS.labels(status=status).set(count)
def set_cluster_state_metrics(
collected_at: datetime,
nodes_total: int | None,
nodes_ready: int | None,
pods_running: float | None,
kustomizations_not_ready: int | None,
) -> None:
def set_cluster_state_metrics(collected_at: datetime, nodes_total: int | None, nodes_ready: int | None, pods_running: float | None, kustomizations_not_ready: int | None) -> None:
"""Set cluster-state gauges from the most recent collector snapshot."""
CLUSTER_STATE_LAST_TS.set(collected_at.timestamp())

View File

@ -5,11 +5,7 @@ import threading
from typing import Any
from ..k8s.client import get_json, patch_json
from ..metrics.metrics import (
record_game_mode_transition,
set_game_mode_managed_replicas,
set_game_mode_state,
)
from ..metrics.metrics import record_game_mode_transition, set_game_mode_managed_replicas, set_game_mode_state
from ..settings import settings
from ..utils.logging import get_logger
@ -45,14 +41,7 @@ class GameModeService:
restore_replicas = int(replicas)
except (TypeError, ValueError):
restore_replicas = 1
workloads.append(
ManagedWorkload(
kind=kind,
namespace=namespace,
name=name,
restore_replicas=max(0, restore_replicas),
)
)
workloads.append(ManagedWorkload(kind, namespace, name, max(0, restore_replicas)))
return workloads
@staticmethod
@ -107,13 +96,7 @@ class GameModeService:
active = bool(workloads) and all(item["desired_replicas"] == 0 for item in workloads)
game = self._current_game or "unknown"
set_game_mode_state(settings.game_mode_node_name, game, active)
return {
"status": "active" if active else "idle",
"active": active,
"node": settings.game_mode_node_name,
"game": game,
"workloads": workloads,
}
return {"status": "active" if active else "idle", "active": active, "node": settings.game_mode_node_name, "game": game, "workloads": workloads}
def start(self, game: str | None = None, note: str | None = None) -> dict[str, Any]:
game_name = self._game_name(game)
@ -124,10 +107,7 @@ class GameModeService:
self._current_game = game_name
set_game_mode_state(settings.game_mode_node_name, game_name, True)
record_game_mode_transition("start", "ok", game_name)
logger.info(
"game mode started",
extra={"event": "game_mode_start", "game": game_name, "note": note or ""},
)
logger.info("game mode started", extra={"event": "game_mode_start", "game": game_name, "note": note or ""})
result = self.status()
result["action"] = "start"
return result
@ -145,10 +125,7 @@ class GameModeService:
self._current_game = ""
set_game_mode_state(settings.game_mode_node_name, game_name, False)
record_game_mode_transition("stop", "ok", game_name)
logger.info(
"game mode stopped",
extra={"event": "game_mode_stop", "game": game_name, "note": note or ""},
)
logger.info("game mode stopped", extra={"event": "game_mode_stop", "game": game_name, "note": note or ""})
result = self.status()
result["action"] = "stop"
result["game"] = game_name

View File

@ -18,8 +18,7 @@ def _group_values(groups: list[str]) -> set[str]:
values: set[str] = set()
for group in groups:
stripped = group.strip()
if not stripped:
continue
if stripped:
values.add(stripped)
values.add(stripped.lstrip("/"))
return values
@ -42,8 +41,7 @@ class GameStreamProfileService:
if not group.startswith(prefix):
continue
suffix = group[len(prefix) :]
if not suffix:
continue
if suffix:
profile_group = group
profile_id = _slug(suffix, profile_id)
allowed = True

View File

@ -160,9 +160,8 @@ class KeycloakAdminClient:
def find_client(self, client_id: str) -> dict[str, Any] | None:
url = f"{settings.keycloak_admin_url}/admin/realms/{settings.keycloak_realm}/clients"
params = {"clientId": client_id, "max": "1"}
with httpx.Client(timeout=10.0) as client:
resp = client.get(url, params=params, headers=self._headers())
resp = client.get(url, params={"clientId": client_id, "max": "1"}, headers=self._headers())
resp.raise_for_status()
payload = resp.json()
if not isinstance(payload, list) or not payload:
@ -195,17 +194,14 @@ class KeycloakAdminClient:
def find_client_scope_id(self, scope_name: str) -> str | None:
url = f"{settings.keycloak_admin_url}/admin/realms/{settings.keycloak_realm}/client-scopes"
params = {"search": scope_name}
with httpx.Client(timeout=10.0) as client:
resp = client.get(url, params=params, headers=self._headers())
resp = client.get(url, params={"search": scope_name}, headers=self._headers())
resp.raise_for_status()
payload = resp.json()
if not isinstance(payload, list):
return None
for item in payload:
if not isinstance(item, dict):
continue
if item.get("name") == scope_name and item.get("id"):
if isinstance(item, dict) and item.get("name") == scope_name and item.get("id"):
return str(item["id"])
return None

View File

@ -3,10 +3,10 @@ from __future__ import annotations
import secrets
from typing import Any
from .keycloak_admin import keycloak_admin
from .vault import vault
from ..settings import settings
from ..utils.logging import get_logger
from .keycloak_admin import keycloak_admin
from .vault import vault
logger = get_logger(__name__)
@ -67,22 +67,10 @@ class OAuth2ProxyService:
cookie_secret = _valid_cookie_secret(existing.get("cookie_secret")) or secrets.token_hex(16)
vault.write_kv_secret(
settings.wolf_oidc_vault_path,
{
"client_id": client_id,
"client_secret": client_secret,
"cookie_secret": cookie_secret,
},
{"client_id": client_id, "client_secret": client_secret, "cookie_secret": cookie_secret},
)
logger.info(
"wolf oauth2 proxy secret ensured",
extra={"event": "wolf_oidc_ensure", "client_id": client_id, "vault_path": settings.wolf_oidc_vault_path},
)
return {
"status": "ok",
"client_id": client_id,
"base_url": base_url,
"vault_path": settings.wolf_oidc_vault_path,
}
logger.info("wolf oauth2 proxy secret ensured", extra={"event": "wolf_oidc_ensure", "client_id": client_id})
return {"status": "ok", "client_id": client_id, "base_url": base_url, "vault_path": settings.wolf_oidc_vault_path}
def ensure_sunshine(self) -> dict[str, Any]:
return self.ensure_wolf()

View File

@ -167,12 +167,7 @@ def _parse_model_response(raw: str) -> tuple[dict[str, Any], str | None]:
return (parsed if isinstance(parsed, dict) else {}, None)
def _diagnosis_from_model(
bundle: dict[str, Any],
parsed: dict[str, Any],
raw: str,
parse_error: str | None,
) -> dict[str, Any]:
def _diagnosis_from_model(bundle: dict[str, Any], parsed: dict[str, Any], raw: str, parse_error: str | None) -> dict[str, Any]:
summary = bundle.get("summary") if isinstance(bundle.get("summary"), dict) else {}
unknowns = list(bundle.get("unknowns") or []) if isinstance(bundle.get("unknowns"), list) else []
if parse_error:
@ -264,13 +259,7 @@ def _text_value(value: Any, default: str) -> str:
return default
def _safe_text_value(
value: Any,
default: str,
unknowns: list[Any],
field: str,
blocked_jobs: set[str],
) -> str:
def _safe_text_value(value: Any, default: str, unknowns: list[Any], field: str, blocked_jobs: set[str]) -> str:
text = _text_value(value, default)
if not _english_ascii(text):
unknowns.append(f"model_{field}_non_english")

View File

@ -8,6 +8,9 @@ import httpx
from ..settings import settings
from ..utils.logging import get_logger
from .vault_policies import DEV_KV_POLICY as _DEV_KV_POLICY
from .vault_policies import K8S_ROLES as _K8S_ROLES
from .vault_policies import VAULT_ADMIN_POLICY as _VAULT_ADMIN_POLICY
logger = get_logger(__name__)
@ -46,270 +49,6 @@ def _build_policy(read_paths: str, write_paths: str) -> str:
)
return "\n".join(policy_parts).strip() + "\n"
_K8S_ROLES: list[dict[str, str]] = [
{
"role": "outline",
"namespace": "outline",
"service_accounts": "outline-vault",
"read_paths": "outline/* shared/postmark-relay",
"write_paths": "",
},
{
"role": "planka",
"namespace": "planka",
"service_accounts": "planka-vault",
"read_paths": "planka/* shared/postmark-relay",
"write_paths": "",
},
{
"role": "bstein-dev-home",
"namespace": "bstein-dev-home",
"service_accounts": "bstein-dev-home,bstein-dev-home-vault-sync",
"read_paths": "portal/* shared/chat-ai-keys-runtime shared/portal-e2e-client shared/postmark-relay "
"mailu/mailu-initial-account-secret shared/harbor-pull",
"write_paths": "",
},
{
"role": "gitea",
"namespace": "gitea",
"service_accounts": "gitea-vault",
"read_paths": "gitea/*",
"write_paths": "",
},
{
"role": "vaultwarden",
"namespace": "vaultwarden",
"service_accounts": "vaultwarden-vault",
"read_paths": "vaultwarden/* mailu/mailu-initial-account-secret",
"write_paths": "",
},
{
"role": "sso",
"namespace": "sso",
"service_accounts": "sso-vault,sso-vault-sync,mas-secrets-ensure",
"read_paths": "sso/* portal/bstein-dev-home-keycloak-admin shared/keycloak-admin "
"shared/portal-e2e-client shared/postmark-relay shared/harbor-pull",
"write_paths": "",
},
{
"role": "mailu-mailserver",
"namespace": "mailu-mailserver",
"service_accounts": "mailu-vault-sync",
"read_paths": "mailu/* shared/postmark-relay shared/harbor-pull",
"write_paths": "",
},
{
"role": "harbor",
"namespace": "harbor",
"service_accounts": "harbor-vault-sync",
"read_paths": "harbor/* shared/harbor-pull",
"write_paths": "",
},
{
"role": "nextcloud",
"namespace": "nextcloud",
"service_accounts": "nextcloud-vault",
"read_paths": "nextcloud/* shared/keycloak-admin shared/postmark-relay",
"write_paths": "",
},
{
"role": "comms",
"namespace": "comms",
"service_accounts": "comms-vault,atlasbot",
"read_paths": "comms/* shared/chat-ai-keys-runtime shared/harbor-pull",
"write_paths": "",
},
{
"role": "jenkins",
"namespace": "jenkins",
"service_accounts": "jenkins",
"read_paths": "jenkins/*",
"write_paths": "",
},
{
"role": "monitoring",
"namespace": "monitoring",
"service_accounts": "monitoring-vault-sync",
"read_paths": "monitoring/* shared/postmark-relay shared/harbor-pull",
"write_paths": "",
},
{
"role": "logging",
"namespace": "logging",
"service_accounts": "logging-vault-sync",
"read_paths": "logging/* shared/harbor-pull",
"write_paths": "",
},
{
"role": "pegasus",
"namespace": "jellyfin",
"service_accounts": "pegasus-vault-sync",
"read_paths": "pegasus/* shared/harbor-pull",
"write_paths": "",
},
{
"role": "crypto",
"namespace": "crypto",
"service_accounts": "crypto-vault-sync",
"read_paths": "crypto/* shared/harbor-pull",
"write_paths": "",
},
{
"role": "health",
"namespace": "health",
"service_accounts": "health-vault-sync",
"read_paths": "health/*",
"write_paths": "",
},
{
"role": "game-stream",
"namespace": "game-stream",
"service_accounts": "game-stream-vault",
"read_paths": "game-stream/*",
"write_paths": "",
},
{
"role": "maintenance",
"namespace": "maintenance",
"service_accounts": "ariadne,maintenance-vault-sync",
"read_paths": "maintenance/ariadne-db portal/bstein-dev-home-keycloak-admin mailu/mailu-db-secret "
"mailu/mailu-initial-account-secret comms/synapse-admin shared/harbor-pull",
"write_paths": "",
},
{
"role": "finance",
"namespace": "finance",
"service_accounts": "finance-vault",
"read_paths": "finance/* shared/postmark-relay",
"write_paths": "",
},
{
"role": "finance-secrets",
"namespace": "finance",
"service_accounts": "finance-secrets-ensure",
"read_paths": "",
"write_paths": "finance/*",
},
{
"role": "longhorn",
"namespace": "longhorn-system",
"service_accounts": "longhorn-vault,longhorn-vault-sync",
"read_paths": "longhorn/* shared/harbor-pull",
"write_paths": "",
},
{
"role": "postgres",
"namespace": "postgres",
"service_accounts": "postgres-vault",
"read_paths": "postgres/postgres-db",
"write_paths": "",
},
{
"role": "vault",
"namespace": "vault",
"service_accounts": "vault",
"read_paths": "vault/*",
"write_paths": "",
},
{
"role": "sso-secrets",
"namespace": "sso",
"service_accounts": "mas-secrets-ensure",
"read_paths": "shared/keycloak-admin",
"write_paths": "harbor/harbor-oidc vault/vault-oidc-config comms/synapse-oidc "
"logging/oauth2-proxy-logs-oidc finance/actual-oidc",
},
{
"role": "crypto-secrets",
"namespace": "crypto",
"service_accounts": "crypto-secrets-ensure",
"read_paths": "",
"write_paths": "crypto/wallet-monero-temp-rpc-auth",
},
{
"role": "comms-secrets",
"namespace": "comms",
"service_accounts": "comms-secrets-ensure,mas-db-ensure,mas-admin-client-secret-writer,othrys-synapse-signingkey-job",
"read_paths": "",
"write_paths": "comms/turn-shared-secret comms/livekit-api comms/synapse-redis comms/synapse-macaroon "
"comms/atlasbot-credentials-runtime comms/synapse-db comms/synapse-admin comms/synapse-registration "
"comms/mas-db comms/mas-admin-client-runtime comms/mas-secrets-runtime comms/othrys-synapse-signingkey",
},
]
_VAULT_ADMIN_POLICY = """
path "sys/auth" {
capabilities = ["read"]
}
path "sys/auth/*" {
capabilities = ["create", "update", "delete", "sudo", "read"]
}
path "auth/kubernetes/*" {
capabilities = ["create", "update", "read"]
}
path "auth/oidc/*" {
capabilities = ["create", "update", "read"]
}
path "sys/policies/acl" {
capabilities = ["list"]
}
path "sys/policies/acl/*" {
capabilities = ["create", "update", "read"]
}
path "sys/internal/ui/mounts" {
capabilities = ["read"]
}
path "sys/mounts" {
capabilities = ["read"]
}
path "sys/mounts/auth/*" {
capabilities = ["read", "update", "sudo"]
}
path "kv/data/atlas/vault/*" {
capabilities = ["read"]
}
path "kv/metadata/atlas/vault/*" {
capabilities = ["list"]
}
path "kv/data/*" {
capabilities = ["create", "update", "read", "delete", "patch"]
}
path "kv/metadata" {
capabilities = ["list"]
}
path "kv/metadata/*" {
capabilities = ["read", "list", "delete"]
}
path "kv/data/atlas/shared/*" {
capabilities = ["create", "update", "read", "patch"]
}
path "kv/metadata/atlas/shared/*" {
capabilities = ["list"]
}
""".strip()
_DEV_KV_POLICY = """
path "kv/metadata" {
capabilities = ["list"]
}
path "kv/metadata/atlas" {
capabilities = ["list"]
}
path "kv/metadata/atlas/shared" {
capabilities = ["list"]
}
path "kv/metadata/atlas/shared/*" {
capabilities = ["list"]
}
path "kv/data/atlas/shared/*" {
capabilities = ["read"]
}
""".strip()
class VaultClient:
"""Minimal HTTP client for Vault API requests."""

View File

@ -117,6 +117,13 @@ K8S_ROLES: list[dict[str, str]] = [
"read_paths": "health/*",
"write_paths": "",
},
{
"role": "game-stream",
"namespace": "game-stream",
"service_accounts": "game-stream-vault",
"read_paths": "game-stream/*",
"write_paths": "",
},
{
"role": "maintenance",
"namespace": "maintenance",

View File

@ -1,47 +1,30 @@
from __future__ import annotations
from dataclasses import dataclass
import json
import os
from typing import Any
def _env(name: str, default: str = "") -> str:
value = os.getenv(name, default)
return value.strip() if isinstance(value, str) else default
def _env_bool(name: str, default: str = "false") -> bool:
return _env(name, default).lower() in {"1", "true", "yes", "y", "on"}
def _env_int(name: str, default: int) -> int:
raw = _env(name, str(default))
try:
return int(raw)
except ValueError:
return default
def _env_float(name: str, default: float) -> float:
raw = _env(name, str(default))
try:
return float(raw)
except ValueError:
return default
def _env_json_list(name: str, default: list[dict[str, Any]]) -> list[dict[str, Any]]:
raw = _env(name, "")
if not raw:
return default
try:
parsed = json.loads(raw)
except json.JSONDecodeError:
return default
if not isinstance(parsed, list):
return default
return [item for item in parsed if isinstance(item, dict)]
from .settings_env import _env, _env_bool, _env_float, _env_int
from .settings_sections import (
_cluster_state_config,
_comms_config,
_firefly_config,
_game_stream_config,
_image_sweeper_config,
_jenkins_build_weather_config,
_jenkins_workspace_cleanup_config,
_keycloak_config,
_mailu_config,
_metis_config,
_nextcloud_config,
_opensearch_config,
_platform_quality_probe_config,
_portal_group_config,
_schedule_config,
_smtp_config,
_testing_triage_config,
_vault_config,
_vaultwarden_config,
_wger_config,
)
@dataclass(frozen=True)
@ -191,6 +174,9 @@ class Settings:
jenkins_workspace_cleanup_min_age_hours: float
jenkins_workspace_cleanup_dry_run: bool
jenkins_workspace_cleanup_max_deletions_per_run: int
testing_triage_model_url: str
testing_triage_model: str
testing_triage_model_timeout_sec: float
vaultwarden_namespace: str
vaultwarden_pod_label: str
@ -245,9 +231,6 @@ class Settings:
cluster_state_keep: int
game_mode_node_name: str
game_mode_displace_workloads: list[dict[str, Any]]
game_mode_ollama_namespace: str
game_mode_ollama_deployment: str
game_mode_ollama_restore_replicas: int
game_mode_hook_token: str
wolf_oidc_client_id: str
wolf_oidc_base_url: str
@ -272,6 +255,7 @@ class Settings:
platform_quality_suite_probe_cron: str
jenkins_build_weather_cron: str
jenkins_workspace_cleanup_cron: str
testing_triage_cron: str
opensearch_url: str
opensearch_limit_bytes: int
@ -280,394 +264,28 @@ class Settings:
metrics_path: str
@classmethod
def _keycloak_config(cls) -> dict[str, Any]:
keycloak_url = _env("KEYCLOAK_URL", "https://sso.bstein.dev").rstrip("/")
keycloak_realm = _env("KEYCLOAK_REALM", "atlas")
keycloak_client_id = _env("KEYCLOAK_CLIENT_ID", "bstein-dev-home")
keycloak_issuer = _env("KEYCLOAK_ISSUER", f"{keycloak_url}/realms/{keycloak_realm}").rstrip("/")
keycloak_jwks_url = _env("KEYCLOAK_JWKS_URL", f"{keycloak_issuer}/protocol/openid-connect/certs").rstrip("/")
return {
"keycloak_url": keycloak_url,
"keycloak_realm": keycloak_realm,
"keycloak_client_id": keycloak_client_id,
"keycloak_issuer": keycloak_issuer,
"keycloak_jwks_url": keycloak_jwks_url,
"keycloak_admin_url": _env("KEYCLOAK_ADMIN_URL", keycloak_url).rstrip("/"),
"keycloak_admin_realm": _env("KEYCLOAK_ADMIN_REALM", keycloak_realm),
"keycloak_admin_client_id": _env("KEYCLOAK_ADMIN_CLIENT_ID", ""),
"keycloak_admin_client_secret": _env("KEYCLOAK_ADMIN_CLIENT_SECRET", ""),
}
@classmethod
def _portal_group_config(cls) -> dict[str, Any]:
return {
"portal_admin_users": [u for u in (_env("PORTAL_ADMIN_USERS", "bstein")).split(",") if u.strip()],
"portal_admin_groups": [g for g in (_env("PORTAL_ADMIN_GROUPS", "admin")).split(",") if g.strip()],
"account_allowed_groups": [
g for g in (_env("ACCOUNT_ALLOWED_GROUPS", "dev,admin")).split(",") if g.strip()
],
"allowed_flag_groups": [g for g in (_env("ALLOWED_FLAG_GROUPS", "demo,test")).split(",") if g.strip()],
"default_user_groups": [g for g in (_env("DEFAULT_USER_GROUPS", "dev")).split(",") if g.strip()],
}
@classmethod
def _mailu_config(cls) -> dict[str, Any]:
mailu_domain = _env("MAILU_DOMAIN", "bstein.dev")
return {
"mailu_domain": mailu_domain,
"mailu_sync_url": _env(
"MAILU_SYNC_URL",
"http://mailu-sync-listener.mailu-mailserver.svc.cluster.local:8080/events",
).rstrip("/"),
"mailu_event_min_interval_sec": _env_float("MAILU_EVENT_MIN_INTERVAL_SEC", 10.0),
"mailu_sync_wait_timeout_sec": _env_float("MAILU_SYNC_WAIT_TIMEOUT_SEC", 60.0),
"mailu_mailbox_wait_timeout_sec": _env_float("MAILU_MAILBOX_WAIT_TIMEOUT_SEC", 60.0),
"mailu_db_host": _env("MAILU_DB_HOST", "postgres-service.postgres.svc.cluster.local"),
"mailu_db_port": _env_int("MAILU_DB_PORT", 5432),
"mailu_db_name": _env("MAILU_DB_NAME", "mailu"),
"mailu_db_user": _env("MAILU_DB_USER", "mailu"),
"mailu_db_password": _env("MAILU_DB_PASSWORD", ""),
"mailu_host": _env("MAILU_HOST", f"mail.{mailu_domain}"),
"mailu_default_quota": _env_int("MAILU_DEFAULT_QUOTA", 20000000000),
"mailu_system_users": [u for u in _env("MAILU_SYSTEM_USERS", "").split(",") if u.strip()],
"mailu_system_password": _env("MAILU_SYSTEM_PASSWORD", ""),
}
@classmethod
def _smtp_config(cls, mailu_domain: str) -> dict[str, Any]:
return {
"smtp_host": _env("SMTP_HOST", ""),
"smtp_port": _env_int("SMTP_PORT", 25),
"smtp_username": _env("SMTP_USERNAME", ""),
"smtp_password": _env("SMTP_PASSWORD", ""),
"smtp_starttls": _env_bool("SMTP_STARTTLS", "false"),
"smtp_use_tls": _env_bool("SMTP_USE_TLS", "false"),
"smtp_from": _env("SMTP_FROM", f"postmaster@{mailu_domain}"),
"smtp_timeout_sec": _env_float("SMTP_TIMEOUT_SEC", 10.0),
"welcome_email_enabled": _env_bool("WELCOME_EMAIL_ENABLED", "true"),
}
@classmethod
def _nextcloud_config(cls) -> dict[str, Any]:
return {
"nextcloud_namespace": _env("NEXTCLOUD_NAMESPACE", "nextcloud"),
"nextcloud_pod_label": _env("NEXTCLOUD_POD_LABEL", "app=nextcloud"),
"nextcloud_container": _env("NEXTCLOUD_CONTAINER", "nextcloud"),
"nextcloud_exec_timeout_sec": _env_float("NEXTCLOUD_EXEC_TIMEOUT_SEC", 120.0),
"nextcloud_db_host": _env("NEXTCLOUD_DB_HOST", "postgres-service.postgres.svc.cluster.local"),
"nextcloud_db_port": _env_int("NEXTCLOUD_DB_PORT", 5432),
"nextcloud_db_name": _env("NEXTCLOUD_DB_NAME", "nextcloud"),
"nextcloud_db_user": _env("NEXTCLOUD_DB_USER", "nextcloud"),
"nextcloud_db_password": _env("NEXTCLOUD_DB_PASSWORD", ""),
"nextcloud_url": _env("NEXTCLOUD_URL", "https://cloud.bstein.dev").rstrip("/"),
"nextcloud_admin_user": _env("NEXTCLOUD_ADMIN_USER", ""),
"nextcloud_admin_password": _env("NEXTCLOUD_ADMIN_PASSWORD", ""),
}
@classmethod
def _wger_config(cls) -> dict[str, Any]:
return {
"wger_namespace": _env("WGER_NAMESPACE", "health"),
"wger_user_sync_wait_timeout_sec": _env_float("WGER_USER_SYNC_WAIT_TIMEOUT_SEC", 60.0),
"wger_pod_label": _env("WGER_POD_LABEL", "app=wger"),
"wger_container": _env("WGER_CONTAINER", "wger"),
"wger_admin_username": _env("WGER_ADMIN_USERNAME", ""),
"wger_admin_password": _env("WGER_ADMIN_PASSWORD", ""),
"wger_admin_email": _env("WGER_ADMIN_EMAIL", ""),
}
@classmethod
def _firefly_config(cls) -> dict[str, Any]:
return {
"firefly_namespace": _env("FIREFLY_NAMESPACE", "finance"),
"firefly_user_sync_wait_timeout_sec": _env_float("FIREFLY_USER_SYNC_WAIT_TIMEOUT_SEC", 90.0),
"firefly_pod_label": _env("FIREFLY_POD_LABEL", "app=firefly"),
"firefly_container": _env("FIREFLY_CONTAINER", "firefly"),
"firefly_cron_base_url": _env(
"FIREFLY_CRON_BASE_URL",
"http://firefly.finance.svc.cluster.local/api/v1/cron",
),
"firefly_cron_token": _env("FIREFLY_CRON_TOKEN", ""),
"firefly_cron_timeout_sec": _env_float("FIREFLY_CRON_TIMEOUT_SEC", 30.0),
}
@classmethod
def _vault_config(cls) -> dict[str, Any]:
return {
"vault_namespace": _env("VAULT_NAMESPACE", "vault"),
"vault_addr": _env("VAULT_ADDR", "http://vault.vault.svc.cluster.local:8200").rstrip("/"),
"vault_token": _env("VAULT_TOKEN", ""),
"vault_k8s_role": _env("VAULT_K8S_ROLE", "vault"),
"vault_k8s_role_ttl": _env("VAULT_K8S_ROLE_TTL", "1h"),
"vault_k8s_token_reviewer_jwt": _env("VAULT_K8S_TOKEN_REVIEWER_JWT", ""),
"vault_k8s_token_reviewer_jwt_file": _env("VAULT_K8S_TOKEN_REVIEWER_JWT_FILE", ""),
"vault_oidc_discovery_url": _env("VAULT_OIDC_DISCOVERY_URL", ""),
"vault_oidc_client_id": _env("VAULT_OIDC_CLIENT_ID", ""),
"vault_oidc_client_secret": _env("VAULT_OIDC_CLIENT_SECRET", ""),
"vault_oidc_default_role": _env("VAULT_OIDC_DEFAULT_ROLE", "admin"),
"vault_oidc_scopes": _env("VAULT_OIDC_SCOPES", "openid profile email groups"),
"vault_oidc_user_claim": _env("VAULT_OIDC_USER_CLAIM", "preferred_username"),
"vault_oidc_groups_claim": _env("VAULT_OIDC_GROUPS_CLAIM", "groups"),
"vault_oidc_token_policies": _env("VAULT_OIDC_TOKEN_POLICIES", ""),
"vault_oidc_admin_group": _env("VAULT_OIDC_ADMIN_GROUP", "admin"),
"vault_oidc_admin_policies": _env("VAULT_OIDC_ADMIN_POLICIES", "default,vault-admin"),
"vault_oidc_dev_group": _env("VAULT_OIDC_DEV_GROUP", "dev"),
"vault_oidc_dev_policies": _env("VAULT_OIDC_DEV_POLICIES", "default,dev-kv"),
"vault_oidc_user_group": _env("VAULT_OIDC_USER_GROUP", ""),
"vault_oidc_user_policies": _env("VAULT_OIDC_USER_POLICIES", ""),
"vault_oidc_redirect_uris": _env(
"VAULT_OIDC_REDIRECT_URIS",
"https://secret.bstein.dev/ui/vault/auth/oidc/oidc/callback",
),
"vault_oidc_bound_audiences": _env("VAULT_OIDC_BOUND_AUDIENCES", ""),
"vault_oidc_bound_claims_type": _env("VAULT_OIDC_BOUND_CLAIMS_TYPE", "string"),
}
@classmethod
def _comms_config(cls) -> dict[str, Any]:
return {
"comms_namespace": _env("COMMS_NAMESPACE", "comms"),
"comms_synapse_base": _env(
"COMMS_SYNAPSE_BASE",
"http://othrys-synapse-matrix-synapse:8008",
).rstrip("/"),
"comms_auth_base": _env(
"COMMS_AUTH_BASE",
"http://matrix-authentication-service:8080",
).rstrip("/"),
"comms_mas_admin_api_base": _env(
"COMMS_MAS_ADMIN_API_BASE",
"http://matrix-authentication-service:8081/api/admin/v1",
).rstrip("/"),
"comms_mas_token_url": _env(
"COMMS_MAS_TOKEN_URL",
"http://matrix-authentication-service:8080/oauth2/token",
),
"comms_mas_admin_client_id": _env("COMMS_MAS_ADMIN_CLIENT_ID", "01KDXMVQBQ5JNY6SEJPZW6Z8BM"),
"comms_mas_admin_client_secret": _env("COMMS_MAS_ADMIN_CLIENT_SECRET", ""),
"comms_server_name": _env("COMMS_SERVER_NAME", "live.bstein.dev"),
"comms_room_alias": _env("COMMS_ROOM_ALIAS", "#othrys:live.bstein.dev"),
"comms_room_name": _env("COMMS_ROOM_NAME", "Othrys"),
"comms_pin_message": _env(
"COMMS_PIN_MESSAGE",
"Invite guests: share https://live.bstein.dev/#/room/#othrys:live.bstein.dev?action=join and choose 'Continue' -> 'Join as guest'.",
),
"comms_seeder_user": _env("COMMS_SEEDER_USER", "othrys-seeder"),
"comms_seeder_password": _env("COMMS_SEEDER_PASSWORD", ""),
"comms_bot_user": _env("COMMS_BOT_USER", "atlasbot"),
"comms_bot_password": _env("COMMS_BOT_PASSWORD", ""),
"comms_synapse_db_host": _env(
"COMMS_SYNAPSE_DB_HOST",
"postgres-service.postgres.svc.cluster.local",
),
"comms_synapse_db_port": _env_int("COMMS_SYNAPSE_DB_PORT", 5432),
"comms_synapse_db_name": _env("COMMS_SYNAPSE_DB_NAME", "synapse"),
"comms_synapse_db_user": _env("COMMS_SYNAPSE_DB_USER", "synapse"),
"comms_synapse_db_password": _env("COMMS_SYNAPSE_DB_PASSWORD", ""),
"comms_synapse_admin_token": _env("COMMS_SYNAPSE_ADMIN_TOKEN", ""),
"comms_timeout_sec": _env_float("COMMS_TIMEOUT_SEC", 30.0),
"comms_guest_stale_days": _env_int("COMMS_GUEST_STALE_DAYS", 14),
}
@classmethod
def _image_sweeper_config(cls) -> dict[str, Any]:
return {
"image_sweeper_namespace": _env("IMAGE_SWEEPER_NAMESPACE", "maintenance"),
"image_sweeper_service_account": _env("IMAGE_SWEEPER_SERVICE_ACCOUNT", "node-image-sweeper"),
"image_sweeper_job_ttl_sec": _env_int("IMAGE_SWEEPER_JOB_TTL_SEC", 3600),
"image_sweeper_wait_timeout_sec": _env_float("IMAGE_SWEEPER_WAIT_TIMEOUT_SEC", 1200.0),
}
@classmethod
def _platform_quality_probe_config(cls) -> dict[str, Any]:
return {
"platform_quality_probe_namespace": _env("PLATFORM_QUALITY_PROBE_NAMESPACE", "monitoring"),
"platform_quality_probe_script_configmap": _env(
"PLATFORM_QUALITY_PROBE_SCRIPT_CONFIGMAP",
"platform-quality-suite-probe-script",
),
"platform_quality_probe_image": _env("PLATFORM_QUALITY_PROBE_IMAGE", "curlimages/curl:8.12.1"),
"platform_quality_probe_job_ttl_sec": _env_int("PLATFORM_QUALITY_PROBE_JOB_TTL_SEC", 1800),
"platform_quality_probe_wait_timeout_sec": _env_float("PLATFORM_QUALITY_PROBE_WAIT_TIMEOUT_SEC", 180.0),
"platform_quality_probe_pushgateway_url": _env(
"PLATFORM_QUALITY_PROBE_PUSHGATEWAY_URL",
"http://platform-quality-gateway.monitoring.svc.cluster.local:9091",
).rstrip("/"),
"platform_quality_probe_http_timeout_sec": _env_int("PLATFORM_QUALITY_PROBE_HTTP_TIMEOUT_SECONDS", 12),
}
@classmethod
def _jenkins_build_weather_config(cls) -> dict[str, Any]:
return {
"jenkins_base_url": _env("JENKINS_BASE_URL", "https://ci.bstein.dev").rstrip("/"),
"jenkins_api_user": _env("JENKINS_API_USER", ""),
"jenkins_api_token": _env("JENKINS_API_TOKEN", ""),
"jenkins_api_timeout_sec": _env_float("JENKINS_API_TIMEOUT_SEC", 10.0),
}
@classmethod
def _jenkins_workspace_cleanup_config(cls) -> dict[str, Any]:
return {
"jenkins_workspace_namespace": _env("JENKINS_WORKSPACE_NAMESPACE", "jenkins"),
"jenkins_workspace_pvc_prefix": _env("JENKINS_WORKSPACE_PVC_PREFIX", "pvc-workspace-"),
"jenkins_workspace_cleanup_min_age_hours": _env_float("JENKINS_WORKSPACE_CLEANUP_MIN_AGE_HOURS", 12.0),
"jenkins_workspace_cleanup_dry_run": _env_bool("JENKINS_WORKSPACE_CLEANUP_DRY_RUN", "false"),
"jenkins_workspace_cleanup_max_deletions_per_run": _env_int(
"JENKINS_WORKSPACE_CLEANUP_MAX_DELETIONS_PER_RUN",
20,
),
}
@classmethod
def _vaultwarden_config(cls) -> dict[str, Any]:
return {
"vaultwarden_namespace": _env("VAULTWARDEN_NAMESPACE", "vaultwarden"),
"vaultwarden_pod_label": _env("VAULTWARDEN_POD_LABEL", "app=vaultwarden"),
"vaultwarden_pod_port": _env_int("VAULTWARDEN_POD_PORT", 80),
"vaultwarden_service_host": _env(
"VAULTWARDEN_SERVICE_HOST",
"vaultwarden-service.vaultwarden.svc.cluster.local",
),
"vaultwarden_admin_secret_name": _env("VAULTWARDEN_ADMIN_SECRET_NAME", "vaultwarden-admin"),
"vaultwarden_admin_secret_key": _env("VAULTWARDEN_ADMIN_SECRET_KEY", "ADMIN_TOKEN"),
"vaultwarden_admin_session_ttl_sec": _env_float("VAULTWARDEN_ADMIN_SESSION_TTL_SEC", 300.0),
"vaultwarden_admin_rate_limit_backoff_sec": _env_float("VAULTWARDEN_ADMIN_RATE_LIMIT_BACKOFF_SEC", 600.0),
"vaultwarden_retry_cooldown_sec": _env_float("VAULTWARDEN_RETRY_COOLDOWN_SEC", 1800.0),
"vaultwarden_failure_bailout": _env_int("VAULTWARDEN_FAILURE_BAILOUT", 2),
"vaultwarden_invite_refresh_sec": _env_float("VAULTWARDEN_INVITE_REFRESH_SEC", 86400.0),
}
@classmethod
def _schedule_config(cls) -> dict[str, Any]:
return {
"mailu_sync_cron": _env("ARIADNE_SCHEDULE_MAILU_SYNC", "30 4 * * *"),
"nextcloud_sync_cron": _env("ARIADNE_SCHEDULE_NEXTCLOUD_SYNC", "0 5 * * *"),
"nextcloud_cron": _env("ARIADNE_SCHEDULE_NEXTCLOUD_CRON", "*/5 * * * *"),
"nextcloud_maintenance_cron": _env("ARIADNE_SCHEDULE_NEXTCLOUD_MAINTENANCE", "30 4 * * *"),
"vaultwarden_sync_cron": _env("ARIADNE_SCHEDULE_VAULTWARDEN_SYNC", "0 * * * *"),
"wger_user_sync_cron": _env("ARIADNE_SCHEDULE_WGER_USER_SYNC", "0 5 * * *"),
"wger_admin_cron": _env("ARIADNE_SCHEDULE_WGER_ADMIN", "15 3 * * *"),
"firefly_user_sync_cron": _env("ARIADNE_SCHEDULE_FIREFLY_USER_SYNC", "0 6 * * *"),
"firefly_cron": _env("ARIADNE_SCHEDULE_FIREFLY_CRON", "0 3 * * *"),
"pod_cleaner_cron": _env("ARIADNE_SCHEDULE_POD_CLEANER", "0 * * * *"),
"opensearch_prune_cron": _env("ARIADNE_SCHEDULE_OPENSEARCH_PRUNE", "23 3 * * *"),
"image_sweeper_cron": _env("ARIADNE_SCHEDULE_IMAGE_SWEEPER", "30 4 * * 0"),
"vault_k8s_auth_cron": _env("ARIADNE_SCHEDULE_VAULT_K8S_AUTH", "0 * * * *"),
"vault_oidc_cron": _env("ARIADNE_SCHEDULE_VAULT_OIDC", "0 * * * *"),
"comms_guest_name_cron": _env("ARIADNE_SCHEDULE_COMMS_GUEST_NAME", "*/5 * * * *"),
"comms_pin_invite_cron": _env("ARIADNE_SCHEDULE_COMMS_PIN_INVITE", "*/30 * * * *"),
"comms_reset_room_cron": _env("ARIADNE_SCHEDULE_COMMS_RESET_ROOM", "0 0 1 1 *"),
"comms_seed_room_cron": _env("ARIADNE_SCHEDULE_COMMS_SEED_ROOM", "*/10 * * * *"),
"keycloak_profile_cron": _env("ARIADNE_SCHEDULE_KEYCLOAK_PROFILE", "0 */6 * * *"),
"metis_k3s_token_sync_cron": _env("ARIADNE_SCHEDULE_METIS_K3S_TOKEN_SYNC", "11 */6 * * *"),
"platform_quality_suite_probe_cron": _env(
"ARIADNE_SCHEDULE_PLATFORM_QUALITY_SUITE_PROBE",
"*/15 * * * *",
),
"jenkins_build_weather_cron": _env(
"ARIADNE_SCHEDULE_JENKINS_BUILD_WEATHER",
"*/10 * * * *",
),
"jenkins_workspace_cleanup_cron": _env(
"ARIADNE_SCHEDULE_JENKINS_WORKSPACE_CLEANUP",
"45 */6 * * *",
),
}
@classmethod
def _cluster_state_config(cls) -> dict[str, Any]:
return {
"vm_url": _env(
"ARIADNE_VM_URL",
"http://victoria-metrics-single-server.monitoring.svc.cluster.local:8428",
).rstrip("/"),
"cluster_state_vm_timeout_sec": _env_float("ARIADNE_CLUSTER_STATE_VM_TIMEOUT_SEC", 5.0),
"alertmanager_url": _env("ARIADNE_ALERTMANAGER_URL", "").rstrip("/"),
"cluster_state_cron": _env("ARIADNE_SCHEDULE_CLUSTER_STATE", "*/15 * * * *"),
"cluster_state_keep": _env_int("ARIADNE_CLUSTER_STATE_KEEP", 168),
}
@classmethod
def _game_mode_config(cls) -> dict[str, Any]:
legacy_ollama = {
"kind": "Deployment",
"namespace": _env("GAME_MODE_OLLAMA_NAMESPACE", "openclaw"),
"name": _env("GAME_MODE_OLLAMA_DEPLOYMENT", "openclaw-ollama"),
"restoreReplicas": _env_int("GAME_MODE_OLLAMA_RESTORE_REPLICAS", 1),
}
return {
"game_mode_node_name": _env("GAME_MODE_NODE_NAME", "titan-24"),
"game_mode_displace_workloads": _env_json_list("GAME_MODE_DISPLACE_WORKLOADS", [legacy_ollama]),
"game_mode_ollama_namespace": legacy_ollama["namespace"],
"game_mode_ollama_deployment": legacy_ollama["name"],
"game_mode_ollama_restore_replicas": legacy_ollama["restoreReplicas"],
"game_mode_hook_token": _env("GAME_MODE_HOOK_TOKEN", ""),
"wolf_oidc_client_id": _env("WOLF_OIDC_CLIENT_ID", _env("SUNSHINE_OIDC_CLIENT_ID", "wolf")),
"wolf_oidc_base_url": _env(
"WOLF_OIDC_BASE_URL",
_env("SUNSHINE_OIDC_BASE_URL", "https://wolf.bstein.dev"),
).rstrip("/"),
"wolf_oidc_vault_path": _env("WOLF_OIDC_VAULT_PATH", _env("SUNSHINE_OIDC_VAULT_PATH", "game-stream/wolf-oidc")),
"wolf_oidc_cron": _env("ARIADNE_SCHEDULE_WOLF_OIDC", _env("ARIADNE_SCHEDULE_SUNSHINE_OIDC", "17 */6 * * *")),
"game_stream_user_group": _env("GAME_STREAM_USER_GROUP", "game-stream-users"),
"game_stream_admin_group": _env("GAME_STREAM_ADMIN_GROUP", "admin"),
"game_stream_profile_group_prefix": _env("GAME_STREAM_PROFILE_GROUP_PREFIX", "game-stream-profile-"),
}
@classmethod
def _metis_config(cls) -> dict[str, Any]:
return {
"metis_base_url": _env("METIS_BASE_URL", "http://metis.maintenance.svc.cluster.local").rstrip("/"),
"metis_watch_url": _env("METIS_WATCH_URL", "").rstrip("/"),
"metis_timeout_sec": _env_float("METIS_TIMEOUT_SEC", 10.0),
"metis_sentinel_watch_cron": _env("ARIADNE_SCHEDULE_METIS_SENTINEL_WATCH", "*/15 * * * *"),
"metis_token_sync_namespace": _env("METIS_TOKEN_SYNC_NAMESPACE", "maintenance"),
"metis_token_sync_service_account": _env("METIS_TOKEN_SYNC_SERVICE_ACCOUNT", "metis-token-sync"),
"metis_token_sync_node_name": _env("METIS_TOKEN_SYNC_NODE_NAME", "titan-0a"),
"metis_token_sync_image": _env("METIS_TOKEN_SYNC_IMAGE", "hashicorp/vault:1.17.6"),
"metis_token_sync_job_ttl_sec": _env_int("METIS_TOKEN_SYNC_JOB_TTL_SEC", 1800),
"metis_token_sync_wait_timeout_sec": _env_float("METIS_TOKEN_SYNC_WAIT_TIMEOUT_SEC", 180.0),
"metis_token_sync_vault_addr": _env(
"METIS_TOKEN_SYNC_VAULT_ADDR",
"http://vault.vault.svc.cluster.local:8200",
).rstrip("/"),
"metis_token_sync_vault_k8s_role": _env("METIS_TOKEN_SYNC_VAULT_K8S_ROLE", "maintenance-metis-token-sync"),
}
@classmethod
def _opensearch_config(cls) -> dict[str, Any]:
return {
"opensearch_url": _env(
"OPENSEARCH_URL",
"http://opensearch-master.logging.svc.cluster.local:9200",
).rstrip("/"),
"opensearch_limit_bytes": _env_int("OPENSEARCH_LIMIT_BYTES", 1024**4),
"opensearch_index_patterns": _env("OPENSEARCH_INDEX_PATTERNS", "kube-*,journald-*"),
"opensearch_timeout_sec": _env_float("OPENSEARCH_TIMEOUT_SEC", 30.0),
}
@classmethod
def from_env(cls) -> "Settings":
keycloak_cfg = cls._keycloak_config()
portal_cfg = cls._portal_group_config()
mailu_cfg = cls._mailu_config()
smtp_cfg = cls._smtp_config(mailu_cfg["mailu_domain"])
nextcloud_cfg = cls._nextcloud_config()
wger_cfg = cls._wger_config()
firefly_cfg = cls._firefly_config()
vault_cfg = cls._vault_config()
comms_cfg = cls._comms_config()
image_cfg = cls._image_sweeper_config()
platform_quality_probe_cfg = cls._platform_quality_probe_config()
jenkins_build_weather_cfg = cls._jenkins_build_weather_config()
jenkins_workspace_cleanup_cfg = cls._jenkins_workspace_cleanup_config()
vaultwarden_cfg = cls._vaultwarden_config()
schedule_cfg = cls._schedule_config()
cluster_cfg = cls._cluster_state_config()
game_mode_cfg = cls._game_mode_config()
metis_cfg = cls._metis_config()
opensearch_cfg = cls._opensearch_config()
keycloak_cfg = _keycloak_config()
portal_cfg = _portal_group_config()
mailu_cfg = _mailu_config()
smtp_cfg = _smtp_config(mailu_cfg["mailu_domain"])
nextcloud_cfg = _nextcloud_config()
wger_cfg = _wger_config()
firefly_cfg = _firefly_config()
vault_cfg = _vault_config()
comms_cfg = _comms_config()
image_cfg = _image_sweeper_config()
platform_quality_probe_cfg = _platform_quality_probe_config()
jenkins_build_weather_cfg = _jenkins_build_weather_config()
jenkins_workspace_cleanup_cfg = _jenkins_workspace_cleanup_config()
testing_triage_cfg = _testing_triage_config()
vaultwarden_cfg = _vaultwarden_config()
schedule_cfg = _schedule_config()
cluster_cfg = _cluster_state_config()
game_stream_cfg = _game_stream_config()
metis_cfg = _metis_config()
opensearch_cfg = _opensearch_config()
portal_db = _env("PORTAL_DATABASE_URL", "")
ariadne_db = _env("ARIADNE_DATABASE_URL", portal_db)
@ -705,10 +323,11 @@ class Settings:
**platform_quality_probe_cfg,
**jenkins_build_weather_cfg,
**jenkins_workspace_cleanup_cfg,
**testing_triage_cfg,
**vaultwarden_cfg,
**schedule_cfg,
**cluster_cfg,
**game_mode_cfg,
**game_stream_cfg,
**metis_cfg,
**opensearch_cfg,
)

View File

@ -1,5 +1,6 @@
from __future__ import annotations
import json
from typing import Any
from .settings_env import _env, _env_bool, _env_float, _env_int
@ -307,6 +308,7 @@ def _schedule_config() -> dict[str, Any]:
"ARIADNE_SCHEDULE_TESTING_TRIAGE",
"*/15 * * * *",
),
"wolf_oidc_cron": _env("ARIADNE_SCHEDULE_WOLF_OIDC", _env("ARIADNE_SCHEDULE_SUNSHINE_OIDC", "17 */6 * * *")),
}
@ -323,6 +325,29 @@ def _cluster_state_config() -> dict[str, Any]:
}
def _game_stream_config() -> dict[str, Any]:
raw_workloads = _env("GAME_MODE_DISPLACE_WORKLOADS", "[]")
try:
parsed_workloads = json.loads(raw_workloads)
except json.JSONDecodeError:
parsed_workloads = []
workloads = parsed_workloads if isinstance(parsed_workloads, list) else []
return {
"game_mode_node_name": _env("GAME_MODE_NODE_NAME", "titan-24"),
"game_mode_displace_workloads": [item for item in workloads if isinstance(item, dict)],
"game_mode_hook_token": _env("GAME_MODE_HOOK_TOKEN", ""),
"wolf_oidc_client_id": _env("WOLF_OIDC_CLIENT_ID", _env("SUNSHINE_OIDC_CLIENT_ID", "wolf")),
"wolf_oidc_base_url": _env(
"WOLF_OIDC_BASE_URL",
_env("SUNSHINE_OIDC_BASE_URL", "https://wolf.bstein.dev"),
).rstrip("/"),
"wolf_oidc_vault_path": _env("WOLF_OIDC_VAULT_PATH", _env("SUNSHINE_OIDC_VAULT_PATH", "game-stream/wolf-oidc")),
"game_stream_user_group": _env("GAME_STREAM_USER_GROUP", "game-stream-users"),
"game_stream_admin_group": _env("GAME_STREAM_ADMIN_GROUP", "admin"),
"game_stream_profile_group_prefix": _env("GAME_STREAM_PROFILE_GROUP_PREFIX", "game-stream-profile-"),
}
def _metis_config() -> dict[str, Any]:
return {
"metis_base_url": _env("METIS_BASE_URL", "http://metis.maintenance.svc.cluster.local").rstrip("/"),

File diff suppressed because it is too large Load Diff

View File

@ -6,17 +6,11 @@ from ariadne.services import game_mode as game_mode_module
from ariadne.services.game_mode import GameModeService
def _settings() -> SimpleNamespace:
def _settings(workloads=None) -> SimpleNamespace:
return SimpleNamespace(
game_mode_node_name="titan-24",
game_mode_displace_workloads=[
{
"kind": "Deployment",
"namespace": "openclaw",
"name": "openclaw-ollama",
"restoreReplicas": 1,
}
],
game_mode_displace_workloads=workloads
or [{"kind": "Deployment", "namespace": "openclaw", "name": "openclaw-ollama", "restoreReplicas": 1}],
)
@ -41,62 +35,62 @@ def test_game_mode_start_and_stop_patch_scale(monkeypatch) -> None:
monkeypatch.setattr(game_mode_module, "record_game_mode_transition", lambda *args, **kwargs: None)
svc = GameModeService()
started = svc.start("Arc Raiders")
assert started["active"] is True
assert svc.start("Arc Raiders")["active"] is True
assert calls[-1][1] == {"spec": {"replicas": 0}}
stopped = svc.stop()
assert stopped["active"] is False
assert svc.stop()["active"] is False
assert calls[-1][1] == {"spec": {"replicas": 1}}
def test_game_mode_status_reports_workload(monkeypatch) -> None:
monkeypatch.setattr(game_mode_module, "settings", _settings())
monkeypatch.setattr(
game_mode_module,
"get_json",
lambda _path: {"spec": {"replicas": 0}, "status": {"replicas": 0}},
)
monkeypatch.setattr(game_mode_module, "set_game_mode_state", lambda *args, **kwargs: None)
monkeypatch.setattr(game_mode_module, "set_game_mode_managed_replicas", lambda *args, **kwargs: None)
status = GameModeService().status()
assert status["status"] == "active"
assert status["workloads"][0]["name"] == "openclaw-ollama"
def test_game_mode_supports_statefulset_workload(monkeypatch) -> None:
monkeypatch.setattr(
game_mode_module,
"settings",
SimpleNamespace(
game_mode_node_name="titan-24",
game_mode_displace_workloads=[
{
"kind": "StatefulSet",
"namespace": "hermes",
"name": "hermes-llm",
"restoreReplicas": "2",
}
],
),
)
workload = [{"kind": "StatefulSet", "namespace": "hermes", "name": "hermes-llm", "restoreReplicas": "2"}]
monkeypatch.setattr(game_mode_module, "settings", _settings(workload))
calls: list[str] = []
monkeypatch.setattr(
game_mode_module,
"get_json",
lambda _path: {"spec": {"replicas": 2}, "status": {"replicas": 2}},
)
def fake_patch_json(path, _payload):
calls.append(path)
return {"ok": True}
monkeypatch.setattr(game_mode_module, "patch_json", fake_patch_json)
monkeypatch.setattr(game_mode_module, "get_json", lambda _path: {"spec": {"replicas": 2}, "status": {"replicas": 2}})
monkeypatch.setattr(game_mode_module, "patch_json", lambda path, _payload: calls.append(path) or {"ok": True})
monkeypatch.setattr(game_mode_module, "set_game_mode_state", lambda *args, **kwargs: None)
monkeypatch.setattr(game_mode_module, "set_game_mode_managed_replicas", lambda *args, **kwargs: None)
monkeypatch.setattr(game_mode_module, "record_game_mode_transition", lambda *args, **kwargs: None)
GameModeService().start("wolf")
assert calls == ["/apis/apps/v1/namespaces/hermes/statefulsets/hermes-llm/scale"]
def test_game_mode_ignores_invalid_workloads_and_fallback_replicas(monkeypatch) -> None:
workloads = [
{"kind": "Deployment", "namespace": "", "name": "missing"},
{"kind": "Deployment", "namespace": "openclaw", "name": "ollama", "restoreReplicas": "bad"},
]
monkeypatch.setattr(game_mode_module, "settings", _settings(workloads))
monkeypatch.setattr(game_mode_module, "get_json", lambda _path: {"spec": {"replicas": None}, "status": {}})
monkeypatch.setattr(game_mode_module, "set_game_mode_state", lambda *args, **kwargs: None)
monkeypatch.setattr(game_mode_module, "set_game_mode_managed_replicas", lambda *args, **kwargs: None)
status = GameModeService().status()
assert status["workloads"][0]["restore_replicas"] == 1
assert status["workloads"][0]["desired_replicas"] is None
def test_game_mode_rejects_unsupported_kind(monkeypatch) -> None:
monkeypatch.setattr(game_mode_module, "settings", _settings([{"kind": "Job", "namespace": "x", "name": "y"}]))
monkeypatch.setattr(game_mode_module, "record_game_mode_transition", lambda *args, **kwargs: None)
try:
GameModeService().start("arc")
except ValueError as exc:
assert "unsupported" in str(exc)
else:
raise AssertionError("unsupported kind should fail")
def test_game_mode_records_stop_errors(monkeypatch) -> None:
monkeypatch.setattr(game_mode_module, "settings", _settings())
transitions = []
monkeypatch.setattr(game_mode_module, "record_game_mode_transition", lambda *args: transitions.append(args))
monkeypatch.setattr(game_mode_module, "patch_json", lambda *_args, **_kwargs: (_ for _ in ()).throw(RuntimeError("boom")))
try:
GameModeService().stop("arc")
except RuntimeError:
pass
assert transitions[-1] == ("stop", "error", "arc")

View File

@ -0,0 +1,10 @@
from __future__ import annotations
from ariadne.metrics import metrics
def test_game_mode_metric_helpers() -> None:
metrics.set_game_mode_state("", "", True)
metrics.record_game_mode_transition("", "", "")
metrics.set_game_mode_managed_replicas("openclaw", "ollama", 0)
metrics.set_game_mode_managed_replicas("openclaw", "ollama", None)

View File

@ -16,19 +16,14 @@ def _settings() -> SimpleNamespace:
def test_profile_defaults_to_user_profile(monkeypatch) -> None:
monkeypatch.setattr(profile_module, "settings", _settings())
profile = GameStreamProfileService().profile_for("Brad Stein", ["game-stream-users"])
assert profile["allowed"] is True
assert profile["profile_id"] == "user-brad-stein"
assert profile["profile_group"] == ""
def test_profile_group_overrides_user_profile(monkeypatch) -> None:
monkeypatch.setattr(profile_module, "settings", _settings())
profile = GameStreamProfileService().profile_for("brad", ["/game-stream-profile-family"])
assert profile["allowed"] is True
assert profile["profile_id"] == "family"
assert profile["profile_group"] == "game-stream-profile-family"
@ -36,8 +31,6 @@ def test_profile_group_overrides_user_profile(monkeypatch) -> None:
def test_profile_denies_unlisted_user(monkeypatch) -> None:
monkeypatch.setattr(profile_module, "settings", _settings())
profile = GameStreamProfileService().profile_for("guest", ["other"])
assert profile["allowed"] is False
assert profile["profile_id"] == "user-guest"

View File

@ -70,16 +70,23 @@ def test_post_json_success(monkeypatch) -> None:
assert result == {"ok": True}
def test_patch_json_success(monkeypatch) -> None:
def test_patch_json_uses_merge_patch_header(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(k8s_api_timeout_sec=5.0)
observed_headers = {}
monkeypatch.setattr(k8s_client, "settings", dummy_settings)
monkeypatch.setattr(k8s_client, "_read_service_account", lambda: ("token", "/tmp/ca"))
client = DummyClient()
monkeypatch.setattr(k8s_client.httpx, "Client", lambda *args, **kwargs: client)
class HeaderClient(DummyClient):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
observed_headers.update(kwargs.get("headers") or {})
monkeypatch.setattr(k8s_client.httpx, "Client", HeaderClient)
result = k8s_client.patch_json("/api/test", {"spec": {"replicas": 0}})
assert result == {"ok": True}
assert client.calls[0][0] == "PATCH"
assert observed_headers["Content-Type"] == "application/merge-patch+json"
def test_patch_json_rejects_non_dict(monkeypatch) -> None:
@ -91,7 +98,7 @@ def test_patch_json_rejects_non_dict(monkeypatch) -> None:
monkeypatch.setattr(k8s_client.httpx, "Client", lambda *args, **kwargs: client)
with pytest.raises(RuntimeError):
k8s_client.patch_json("/api/test", {"spec": {"replicas": 0}})
k8s_client.patch_json("/api/test", {"payload": "ok"})
def test_get_json_rejects_non_dict(monkeypatch) -> None:

View File

@ -1,768 +0,0 @@
from __future__ import annotations
from typing import Any
import types
import httpx
import pytest
from ariadne.services.keycloak_admin import KeycloakAdminClient
class DummyResponse:
def __init__(self, payload=None, status_code=200, headers=None):
self._payload = payload
self.status_code = status_code
self.headers = headers or {}
def json(self):
return self._payload
def raise_for_status(self):
if self.status_code >= 400:
request = httpx.Request("GET", "https://example.com")
response = httpx.Response(self.status_code, request=request)
raise httpx.HTTPStatusError("error", request=request, response=response)
class DummyClient:
def __init__(self, responses):
self._responses = list(responses)
self.calls = []
def __enter__(self):
return self
def __exit__(self, exc_type, exc, tb):
return False
def _next(self):
if not self._responses:
raise RuntimeError("missing response")
return self._responses.pop(0)
def get(self, url, params=None, headers=None):
self.calls.append(("get", url, params))
return self._next()
def post(self, url, data=None, json=None, headers=None):
self.calls.append(("post", url, data, json))
return self._next()
def put(self, url, headers=None, json=None):
self.calls.append(("put", url, json))
return self._next()
def test_set_user_attribute_preserves_profile(monkeypatch) -> None:
client = KeycloakAdminClient()
captured: dict[str, Any] = {}
def fake_find_user(username: str) -> dict[str, Any]:
return {"id": "user-123"}
def fake_get_user(user_id: str) -> dict[str, Any]:
return {
"id": user_id,
"username": "alice",
"email": "alice@bstein.dev",
"emailVerified": True,
"enabled": True,
"firstName": "Alice",
"lastName": "Smith",
"requiredActions": ["UPDATE_PASSWORD", 123],
"attributes": {"existing": ["value"]},
}
def fake_update_user(user_id: str, payload: dict[str, Any]) -> None:
captured["user_id"] = user_id
captured["payload"] = payload
monkeypatch.setattr(client, "find_user", fake_find_user)
monkeypatch.setattr(client, "get_user", fake_get_user)
monkeypatch.setattr(client, "update_user", fake_update_user)
client.set_user_attribute("alice", "mailu_app_password", "secret")
payload = captured.get("payload") or {}
assert payload.get("username") == "alice"
assert payload.get("email") == "alice@bstein.dev"
assert payload.get("emailVerified") is True
assert payload.get("enabled") is True
assert payload.get("firstName") == "Alice"
assert payload.get("lastName") == "Smith"
assert payload.get("requiredActions") == ["UPDATE_PASSWORD"]
assert payload.get("attributes") == {
"existing": ["value"],
"mailu_app_password": ["secret"],
}
def test_update_user_safe_merges_payload(monkeypatch) -> None:
client = KeycloakAdminClient()
captured: dict[str, Any] = {}
def fake_get_user(user_id: str) -> dict[str, Any]:
return {
"id": user_id,
"username": "alice",
"enabled": True,
"attributes": {"existing": ["value"]},
}
def fake_update_user(user_id: str, payload: dict[str, Any]) -> None:
captured["user_id"] = user_id
captured["payload"] = payload
monkeypatch.setattr(client, "get_user", fake_get_user)
monkeypatch.setattr(client, "update_user", fake_update_user)
client.update_user_safe(
"user-123",
{"attributes": {"new": ["item"]}, "requiredActions": ["UPDATE_PASSWORD"]},
)
payload = captured.get("payload") or {}
assert payload.get("username") == "alice"
assert payload.get("attributes") == {"existing": ["value"], "new": ["item"]}
assert payload.get("requiredActions") == ["UPDATE_PASSWORD"]
def test_get_token_fetches_once(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
keycloak_admin_url="http://kc",
keycloak_admin_realm="atlas",
keycloak_admin_client_id="client",
keycloak_admin_client_secret="secret",
keycloak_realm="atlas",
)
monkeypatch.setattr("ariadne.services.keycloak_admin.settings", dummy_settings)
client = KeycloakAdminClient()
dummy = DummyClient([DummyResponse({"access_token": "token", "expires_in": 120})])
monkeypatch.setattr("ariadne.services.keycloak_admin.httpx.Client", lambda *args, **kwargs: dummy)
assert client._get_token() == "token"
monkeypatch.setattr("ariadne.services.keycloak_admin.httpx.Client", lambda *args, **kwargs: (_ for _ in ()).throw(RuntimeError("should not call")))
assert client._get_token() == "token"
def test_find_user_by_email_case_insensitive(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
keycloak_admin_url="http://kc",
keycloak_admin_realm="atlas",
keycloak_admin_client_id="client",
keycloak_admin_client_secret="secret",
keycloak_realm="atlas",
)
monkeypatch.setattr("ariadne.services.keycloak_admin.settings", dummy_settings)
client = KeycloakAdminClient()
client._token = "token"
client._expires_at = 9999999999
dummy = DummyClient([DummyResponse([{"email": "Alice@Example.com", "id": "1"}])])
monkeypatch.setattr("ariadne.services.keycloak_admin.httpx.Client", lambda *args, **kwargs: dummy)
user = client.find_user_by_email("alice@example.com")
assert user["id"] == "1"
def test_find_user_invalid_payload(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
keycloak_admin_url="http://kc",
keycloak_admin_realm="atlas",
keycloak_admin_client_id="client",
keycloak_admin_client_secret="secret",
keycloak_realm="atlas",
)
monkeypatch.setattr("ariadne.services.keycloak_admin.settings", dummy_settings)
client = KeycloakAdminClient()
client._token = "token"
client._expires_at = 9999999999
dummy = DummyClient([DummyResponse(["bad"])])
monkeypatch.setattr("ariadne.services.keycloak_admin.httpx.Client", lambda *args, **kwargs: dummy)
assert client.find_user("alice") is None
def test_find_user_by_email_empty() -> None:
client = KeycloakAdminClient()
assert client.find_user_by_email("") is None
def test_find_user_by_email_invalid_payload(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
keycloak_admin_url="http://kc",
keycloak_admin_realm="atlas",
keycloak_admin_client_id="client",
keycloak_admin_client_secret="secret",
keycloak_realm="atlas",
)
monkeypatch.setattr("ariadne.services.keycloak_admin.settings", dummy_settings)
client = KeycloakAdminClient()
client._token = "token"
client._expires_at = 9999999999
dummy = DummyClient([DummyResponse({"bad": "payload"})])
monkeypatch.setattr("ariadne.services.keycloak_admin.httpx.Client", lambda *args, **kwargs: dummy)
assert client.find_user_by_email("alice@example.com") is None
def test_list_group_names_filters(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
keycloak_admin_url="http://kc",
keycloak_admin_realm="atlas",
keycloak_admin_client_id="client",
keycloak_admin_client_secret="secret",
keycloak_realm="atlas",
)
monkeypatch.setattr("ariadne.services.keycloak_admin.settings", dummy_settings)
client = KeycloakAdminClient()
client._token = "token"
client._expires_at = 9999999999
dummy = DummyClient([DummyResponse([{"name": "demo"}, {"name": "admin"}, {"name": "test"}])])
monkeypatch.setattr("ariadne.services.keycloak_admin.httpx.Client", lambda *args, **kwargs: dummy)
assert client.list_group_names(exclude={"admin"}) == ["demo", "test"]
def test_find_user_by_email_skips_non_dict(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
keycloak_admin_url="http://kc",
keycloak_admin_realm="atlas",
keycloak_admin_client_id="client",
keycloak_admin_client_secret="secret",
keycloak_realm="atlas",
)
monkeypatch.setattr("ariadne.services.keycloak_admin.settings", dummy_settings)
client = KeycloakAdminClient()
client._token = "token"
client._expires_at = 9999999999
dummy = DummyClient([DummyResponse(["bad"])])
monkeypatch.setattr("ariadne.services.keycloak_admin.httpx.Client", lambda *args, **kwargs: dummy)
assert client.find_user_by_email("alice@example.com") is None
def test_get_user_invalid_payload(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
keycloak_admin_url="http://kc",
keycloak_admin_realm="atlas",
keycloak_admin_client_id="client",
keycloak_admin_client_secret="secret",
keycloak_realm="atlas",
)
monkeypatch.setattr("ariadne.services.keycloak_admin.settings", dummy_settings)
client = KeycloakAdminClient()
client._token = "token"
client._expires_at = 9999999999
dummy = DummyClient([DummyResponse("bad")])
monkeypatch.setattr("ariadne.services.keycloak_admin.httpx.Client", lambda *args, **kwargs: dummy)
with pytest.raises(RuntimeError):
client.get_user("user-1")
def test_update_user_calls_put(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
keycloak_admin_url="http://kc",
keycloak_admin_realm="atlas",
keycloak_admin_client_id="client",
keycloak_admin_client_secret="secret",
keycloak_realm="atlas",
)
monkeypatch.setattr("ariadne.services.keycloak_admin.settings", dummy_settings)
client = KeycloakAdminClient()
client._token = "token"
client._expires_at = 9999999999
dummy = DummyClient([DummyResponse({})])
monkeypatch.setattr("ariadne.services.keycloak_admin.httpx.Client", lambda *args, **kwargs: dummy)
client.update_user("user-1", {"enabled": True})
assert dummy.calls
def test_update_user_safe_handles_bad_attrs(monkeypatch) -> None:
client = KeycloakAdminClient()
captured: dict[str, Any] = {}
def fake_get_user(user_id: str) -> dict[str, Any]:
return {"id": user_id, "username": "alice", "attributes": "bad"}
def fake_update_user(user_id: str, payload: dict[str, Any]) -> None:
captured["payload"] = payload
monkeypatch.setattr(client, "get_user", fake_get_user)
monkeypatch.setattr(client, "update_user", fake_update_user)
client.update_user_safe("user-1", {"attributes": {"new": ["item"]}})
assert captured["payload"]["attributes"] == {"new": ["item"]}
def test_set_user_attribute_user_id_missing(monkeypatch) -> None:
client = KeycloakAdminClient()
def fake_find_user(username: str) -> dict[str, Any]:
return {"id": ""}
monkeypatch.setattr(client, "find_user", fake_find_user)
with pytest.raises(RuntimeError):
client.set_user_attribute("alice", "attr", "val")
def test_set_user_attribute_handles_bad_attrs(monkeypatch) -> None:
client = KeycloakAdminClient()
def fake_find_user(username: str) -> dict[str, Any]:
return {"id": "user-1"}
def fake_get_user(user_id: str) -> dict[str, Any]:
return {"id": user_id, "username": "alice", "attributes": "bad"}
monkeypatch.setattr(client, "find_user", fake_find_user)
monkeypatch.setattr(client, "get_user", fake_get_user)
monkeypatch.setattr(client, "update_user", lambda *_args, **_kwargs: None)
client.set_user_attribute("alice", "attr", "val")
def test_get_group_id_skips_non_dict(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
keycloak_admin_url="http://kc",
keycloak_admin_realm="atlas",
keycloak_admin_client_id="client",
keycloak_admin_client_secret="secret",
keycloak_realm="atlas",
)
monkeypatch.setattr("ariadne.services.keycloak_admin.settings", dummy_settings)
client = KeycloakAdminClient()
client._token = "token"
client._expires_at = 9999999999
dummy = DummyClient([DummyResponse(["bad"])])
monkeypatch.setattr("ariadne.services.keycloak_admin.httpx.Client", lambda *args, **kwargs: dummy)
assert client.get_group_id("demo") is None
def test_get_group_id_cached(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
keycloak_admin_url="http://kc",
keycloak_admin_realm="atlas",
keycloak_admin_client_id="client",
keycloak_admin_client_secret="secret",
keycloak_realm="atlas",
)
monkeypatch.setattr("ariadne.services.keycloak_admin.settings", dummy_settings)
client = KeycloakAdminClient()
client._token = "token"
client._expires_at = 9999999999
dummy = DummyClient([DummyResponse([{"name": "demo", "id": "gid"}])])
monkeypatch.setattr("ariadne.services.keycloak_admin.httpx.Client", lambda *args, **kwargs: dummy)
assert client.get_group_id("demo") == "gid"
monkeypatch.setattr("ariadne.services.keycloak_admin.httpx.Client", lambda *args, **kwargs: (_ for _ in ()).throw(RuntimeError("no call")))
assert client.get_group_id("demo") == "gid"
def test_get_group_id_invalid_payload(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
keycloak_admin_url="http://kc",
keycloak_admin_realm="atlas",
keycloak_admin_client_id="client",
keycloak_admin_client_secret="secret",
keycloak_realm="atlas",
)
monkeypatch.setattr("ariadne.services.keycloak_admin.settings", dummy_settings)
client = KeycloakAdminClient()
client._token = "token"
client._expires_at = 9999999999
dummy = DummyClient([DummyResponse({"bad": "payload"})])
monkeypatch.setattr("ariadne.services.keycloak_admin.httpx.Client", lambda *args, **kwargs: dummy)
assert client.get_group_id("demo") is None
def test_iter_users_paginates(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
keycloak_admin_url="http://kc",
keycloak_admin_realm="atlas",
keycloak_admin_client_id="client",
keycloak_admin_client_secret="secret",
keycloak_realm="atlas",
)
monkeypatch.setattr("ariadne.services.keycloak_admin.settings", dummy_settings)
client = KeycloakAdminClient()
client._token = "token"
client._expires_at = 9999999999
dummy = DummyClient(
[
DummyResponse([{"id": "1"}, {"id": "2"}]),
DummyResponse([{"id": "3"}]),
]
)
monkeypatch.setattr("ariadne.services.keycloak_admin.httpx.Client", lambda *args, **kwargs: dummy)
users = client.iter_users(page_size=2, brief=True)
assert [u["id"] for u in users] == ["1", "2", "3"]
def test_iter_users_empty(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
keycloak_admin_url="http://kc",
keycloak_admin_realm="atlas",
keycloak_admin_client_id="client",
keycloak_admin_client_secret="secret",
keycloak_realm="atlas",
)
monkeypatch.setattr("ariadne.services.keycloak_admin.settings", dummy_settings)
client = KeycloakAdminClient()
client._token = "token"
client._expires_at = 9999999999
dummy = DummyClient([DummyResponse([])])
monkeypatch.setattr("ariadne.services.keycloak_admin.httpx.Client", lambda *args, **kwargs: dummy)
assert client.iter_users(page_size=2) == []
def test_create_user_parses_location(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
keycloak_admin_url="http://kc",
keycloak_admin_realm="atlas",
keycloak_admin_client_id="client",
keycloak_admin_client_secret="secret",
keycloak_realm="atlas",
)
monkeypatch.setattr("ariadne.services.keycloak_admin.settings", dummy_settings)
client = KeycloakAdminClient()
client._token = "token"
client._expires_at = 9999999999
dummy = DummyClient([DummyResponse({}, headers={"Location": "http://kc/admin/realms/atlas/users/abc"})])
monkeypatch.setattr("ariadne.services.keycloak_admin.httpx.Client", lambda *args, **kwargs: dummy)
assert client.create_user({"username": "alice"}) == "abc"
def test_create_user_missing_location(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
keycloak_admin_url="http://kc",
keycloak_admin_realm="atlas",
keycloak_admin_client_id="client",
keycloak_admin_client_secret="secret",
keycloak_realm="atlas",
)
monkeypatch.setattr("ariadne.services.keycloak_admin.settings", dummy_settings)
client = KeycloakAdminClient()
client._token = "token"
client._expires_at = 9999999999
dummy = DummyClient([DummyResponse({}, headers={})])
monkeypatch.setattr("ariadne.services.keycloak_admin.httpx.Client", lambda *args, **kwargs: dummy)
with pytest.raises(RuntimeError):
client.create_user({"username": "alice"})
def test_find_client(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
keycloak_admin_url="http://kc",
keycloak_admin_realm="atlas",
keycloak_admin_client_id="client",
keycloak_admin_client_secret="secret",
keycloak_realm="atlas",
)
monkeypatch.setattr("ariadne.services.keycloak_admin.settings", dummy_settings)
client = KeycloakAdminClient()
client._token = "token"
client._expires_at = 9999999999
dummy = DummyClient([DummyResponse([{"id": "abc", "clientId": "sunshine"}])])
monkeypatch.setattr("ariadne.services.keycloak_admin.httpx.Client", lambda *args, **kwargs: dummy)
assert client.find_client("sunshine") == {"id": "abc", "clientId": "sunshine"}
def test_find_client_missing(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
keycloak_admin_url="http://kc",
keycloak_admin_realm="atlas",
keycloak_admin_client_id="client",
keycloak_admin_client_secret="secret",
keycloak_realm="atlas",
)
monkeypatch.setattr("ariadne.services.keycloak_admin.settings", dummy_settings)
client = KeycloakAdminClient()
client._token = "token"
client._expires_at = 9999999999
dummy = DummyClient([DummyResponse([])])
monkeypatch.setattr("ariadne.services.keycloak_admin.httpx.Client", lambda *args, **kwargs: dummy)
assert client.find_client("sunshine") is None
def test_client_crud_helpers(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
keycloak_admin_url="http://kc",
keycloak_admin_realm="atlas",
keycloak_admin_client_id="client",
keycloak_admin_client_secret="secret",
keycloak_realm="atlas",
)
monkeypatch.setattr("ariadne.services.keycloak_admin.settings", dummy_settings)
client = KeycloakAdminClient()
client._token = "token"
client._expires_at = 9999999999
dummy = DummyClient(
[
DummyResponse({}),
DummyResponse({}),
DummyResponse({"value": "secret"}),
DummyResponse([{"name": "groups", "id": "scope-id"}]),
DummyResponse({}),
]
)
monkeypatch.setattr("ariadne.services.keycloak_admin.httpx.Client", lambda *args, **kwargs: dummy)
client.create_client({"clientId": "sunshine"})
client.update_client("uuid", {"clientId": "sunshine"})
assert client.get_client_secret("uuid") == "secret"
assert client.find_client_scope_id("groups") == "scope-id"
client.attach_optional_client_scope("uuid", "scope-id")
def test_get_client_secret_missing(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
keycloak_admin_url="http://kc",
keycloak_admin_realm="atlas",
keycloak_admin_client_id="client",
keycloak_admin_client_secret="secret",
keycloak_realm="atlas",
)
monkeypatch.setattr("ariadne.services.keycloak_admin.settings", dummy_settings)
client = KeycloakAdminClient()
client._token = "token"
client._expires_at = 9999999999
dummy = DummyClient([DummyResponse({})])
monkeypatch.setattr("ariadne.services.keycloak_admin.httpx.Client", lambda *args, **kwargs: dummy)
with pytest.raises(RuntimeError):
client.get_client_secret("uuid")
def test_get_token_missing_access_token(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
keycloak_admin_url="http://kc",
keycloak_admin_realm="atlas",
keycloak_admin_client_id="client",
keycloak_admin_client_secret="secret",
keycloak_realm="atlas",
)
monkeypatch.setattr("ariadne.services.keycloak_admin.settings", dummy_settings)
client = KeycloakAdminClient()
dummy = DummyClient([DummyResponse({"expires_in": 120})])
monkeypatch.setattr("ariadne.services.keycloak_admin.httpx.Client", lambda *args, **kwargs: dummy)
with pytest.raises(RuntimeError):
client._get_token()
def test_reset_password_raises_on_error(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
keycloak_admin_url="http://kc",
keycloak_admin_realm="atlas",
keycloak_admin_client_id="client",
keycloak_admin_client_secret="secret",
keycloak_realm="atlas",
)
monkeypatch.setattr("ariadne.services.keycloak_admin.settings", dummy_settings)
client = KeycloakAdminClient()
client._token = "token"
client._expires_at = 9999999999
dummy = DummyClient([DummyResponse({}, status_code=400)])
monkeypatch.setattr("ariadne.services.keycloak_admin.httpx.Client", lambda *args, **kwargs: dummy)
with pytest.raises(httpx.HTTPStatusError):
client.reset_password("user", "pw", temporary=True)
def test_get_token_requires_config(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
keycloak_admin_url="http://kc",
keycloak_admin_realm="atlas",
keycloak_admin_client_id="",
keycloak_admin_client_secret="",
keycloak_realm="atlas",
)
monkeypatch.setattr("ariadne.services.keycloak_admin.settings", dummy_settings)
client = KeycloakAdminClient()
with pytest.raises(RuntimeError):
client._get_token()
def test_headers_includes_bearer(monkeypatch) -> None:
client = KeycloakAdminClient()
monkeypatch.setattr(client, "_get_token", lambda: "token")
headers = client.headers()
assert headers["Authorization"] == "Bearer token"
def test_find_user_returns_none(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
keycloak_admin_url="http://kc",
keycloak_admin_realm="atlas",
keycloak_admin_client_id="client",
keycloak_admin_client_secret="secret",
keycloak_realm="atlas",
)
monkeypatch.setattr("ariadne.services.keycloak_admin.settings", dummy_settings)
client = KeycloakAdminClient()
client._token = "token"
client._expires_at = 9999999999
dummy = DummyClient([DummyResponse([])])
monkeypatch.setattr("ariadne.services.keycloak_admin.httpx.Client", lambda *args, **kwargs: dummy)
assert client.find_user("alice") is None
def test_get_user_invalid_payload(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
keycloak_admin_url="http://kc",
keycloak_admin_realm="atlas",
keycloak_admin_client_id="client",
keycloak_admin_client_secret="secret",
keycloak_realm="atlas",
)
monkeypatch.setattr("ariadne.services.keycloak_admin.settings", dummy_settings)
client = KeycloakAdminClient()
client._token = "token"
client._expires_at = 9999999999
dummy = DummyClient([DummyResponse(["bad"])])
monkeypatch.setattr("ariadne.services.keycloak_admin.httpx.Client", lambda *args, **kwargs: dummy)
with pytest.raises(RuntimeError):
client.get_user("id")
def test_get_user_success(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
keycloak_admin_url="http://kc",
keycloak_admin_realm="atlas",
keycloak_admin_client_id="client",
keycloak_admin_client_secret="secret",
keycloak_realm="atlas",
)
monkeypatch.setattr("ariadne.services.keycloak_admin.settings", dummy_settings)
client = KeycloakAdminClient()
client._token = "token"
client._expires_at = 9999999999
dummy = DummyClient([DummyResponse({"id": "1"})])
monkeypatch.setattr("ariadne.services.keycloak_admin.httpx.Client", lambda *args, **kwargs: dummy)
user = client.get_user("id")
assert user["id"] == "1"
def test_set_user_attribute_user_missing(monkeypatch) -> None:
client = KeycloakAdminClient()
monkeypatch.setattr(client, "find_user", lambda username: None)
with pytest.raises(RuntimeError):
client.set_user_attribute("alice", "attr", "value")
def test_set_user_attribute_user_id_missing(monkeypatch) -> None:
client = KeycloakAdminClient()
monkeypatch.setattr(client, "find_user", lambda username: {})
with pytest.raises(RuntimeError):
client.set_user_attribute("alice", "attr", "value")
def test_add_user_to_group(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
keycloak_admin_url="http://kc",
keycloak_admin_realm="atlas",
keycloak_admin_client_id="client",
keycloak_admin_client_secret="secret",
keycloak_realm="atlas",
)
monkeypatch.setattr("ariadne.services.keycloak_admin.settings", dummy_settings)
client = KeycloakAdminClient()
client._token = "token"
client._expires_at = 9999999999
dummy = DummyClient([DummyResponse({})])
monkeypatch.setattr("ariadne.services.keycloak_admin.httpx.Client", lambda *args, **kwargs: dummy)
client.add_user_to_group("user", "group")
assert dummy.calls[0][0] == "put"
def test_get_user_raises_on_non_dict_payload(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
keycloak_admin_url="http://kc",
keycloak_admin_realm="atlas",
keycloak_admin_client_id="client",
keycloak_admin_client_secret="secret",
keycloak_realm="atlas",
)
monkeypatch.setattr("ariadne.services.keycloak_admin.settings", dummy_settings)
client = KeycloakAdminClient()
client._token = "token"
client._expires_at = 9999999999
dummy = DummyClient([DummyResponse("bad")])
monkeypatch.setattr("ariadne.services.keycloak_admin.httpx.Client", lambda *args, **kwargs: dummy)
with pytest.raises(RuntimeError):
client.get_user("user-1")
def test_update_user_safe_coerces_bad_attrs(monkeypatch) -> None:
client = KeycloakAdminClient()
monkeypatch.setattr(client, "get_user", lambda *_args, **_kwargs: {"id": "user-1"})
monkeypatch.setattr(client, "_safe_update_payload", lambda *_args, **_kwargs: {"attributes": "bad"})
monkeypatch.setattr(client, "update_user", lambda *_args, **_kwargs: None)
client.update_user_safe("user-1", {"attributes": {"new": ["item"]}})
def test_set_user_attribute_coerces_bad_attrs(monkeypatch) -> None:
client = KeycloakAdminClient()
monkeypatch.setattr(client, "find_user", lambda username: {"id": "user-1"})
monkeypatch.setattr(client, "get_user", lambda *_args, **_kwargs: {"id": "user-1"})
monkeypatch.setattr(client, "_safe_update_payload", lambda *_args, **_kwargs: {"attributes": "bad"})
monkeypatch.setattr(client, "update_user", lambda *_args, **_kwargs: None)
client.set_user_attribute("alice", "attr", "value")
def test_set_user_attribute_user_id_missing_raises(monkeypatch) -> None:
client = KeycloakAdminClient()
monkeypatch.setattr(client, "find_user", lambda username: {"id": ""})
with pytest.raises(RuntimeError):
client.set_user_attribute("alice", "attr", "value")
def test_get_user_rejects_non_dict_payload(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
keycloak_admin_url="http://kc",
keycloak_admin_realm="atlas",
keycloak_admin_client_id="client",
keycloak_admin_client_secret="secret",
keycloak_realm="atlas",
)
monkeypatch.setattr("ariadne.services.keycloak_admin.settings", dummy_settings)
client = KeycloakAdminClient()
client._token = "token"
client._expires_at = 9999999999
dummy = DummyClient([DummyResponse(123)])
monkeypatch.setattr("ariadne.services.keycloak_admin.httpx.Client", lambda *args, **kwargs: dummy)
with pytest.raises(RuntimeError) as exc:
client.get_user("user-1")
assert "unexpected user payload" in str(exc.value)

View File

@ -10,7 +10,6 @@ def test_oauth_client_payload() -> None:
payload = _oauth_client_payload("wolf", "https://wolf.bstein.dev")
assert payload["clientId"] == "wolf"
assert payload["redirectUris"] == ["https://wolf.bstein.dev/oauth2/callback"]
assert payload["webOrigins"] == ["https://wolf.bstein.dev"]
def test_valid_cookie_secret() -> None:
@ -23,11 +22,7 @@ def test_ensure_wolf_creates_client_and_writes_vault(monkeypatch) -> None:
monkeypatch.setattr(
oauth_module,
"settings",
SimpleNamespace(
wolf_oidc_client_id="wolf",
wolf_oidc_base_url="https://wolf.bstein.dev",
wolf_oidc_vault_path="game-stream/wolf-oidc",
),
SimpleNamespace(wolf_oidc_client_id="wolf", wolf_oidc_base_url="https://wolf.bstein.dev", wolf_oidc_vault_path="game-stream/wolf-oidc"),
)
calls: list[str] = []
written = {}
@ -40,9 +35,7 @@ def test_ensure_wolf_creates_client_and_writes_vault(monkeypatch) -> None:
return True
def find_client(self, client_id):
if not self.created:
return None
return {"id": "client-uuid", "clientId": client_id}
return {"id": "client-uuid", "clientId": client_id} if self.created else None
def create_client(self, _payload):
self.created = True
@ -51,8 +44,7 @@ def test_ensure_wolf_creates_client_and_writes_vault(monkeypatch) -> None:
def update_client(self, _client_uuid, _payload):
calls.append("update")
def find_client_scope_id(self, name):
assert name == "groups"
def find_client_scope_id(self, _name):
return "scope-uuid"
def attach_optional_client_scope(self, _client_uuid, _scope_id):
@ -62,8 +54,7 @@ def test_ensure_wolf_creates_client_and_writes_vault(monkeypatch) -> None:
return "client-secret"
class DummyVault:
def read_kv_secret(self, path):
assert path == "game-stream/wolf-oidc"
def read_kv_secret(self, _path):
return {"cookie_secret": "a" * 32}
def write_kv_secret(self, path, data):
@ -73,8 +64,7 @@ def test_ensure_wolf_creates_client_and_writes_vault(monkeypatch) -> None:
monkeypatch.setattr(oauth_module, "keycloak_admin", DummyKeycloak())
monkeypatch.setattr(oauth_module, "vault", DummyVault())
result = OAuth2ProxyService().ensure_wolf()
assert result["status"] == "ok"
assert OAuth2ProxyService().ensure_wolf()["status"] == "ok"
assert calls == ["create", "update", "scope"]
assert written["data"]["client_secret"] == "client-secret"
assert written["data"]["cookie_secret"] == "a" * 32
@ -84,12 +74,47 @@ def test_ensure_wolf_reports_missing_keycloak(monkeypatch) -> None:
monkeypatch.setattr(
oauth_module,
"settings",
SimpleNamespace(
wolf_oidc_client_id="wolf",
wolf_oidc_base_url="https://wolf.bstein.dev",
wolf_oidc_vault_path="game-stream/wolf-oidc",
),
SimpleNamespace(wolf_oidc_client_id="wolf", wolf_oidc_base_url="https://wolf.bstein.dev", wolf_oidc_vault_path="game-stream/wolf-oidc"),
)
monkeypatch.setattr(oauth_module, "keycloak_admin", SimpleNamespace(ready=lambda: False))
assert OAuth2ProxyService().ensure_wolf()["status"] == "error"
def test_ensure_wolf_rejects_missing_client_after_create(monkeypatch) -> None:
monkeypatch.setattr(
oauth_module,
"settings",
SimpleNamespace(wolf_oidc_client_id="wolf", wolf_oidc_base_url="https://wolf.bstein.dev", wolf_oidc_vault_path="game-stream/wolf-oidc"),
)
monkeypatch.setattr(
oauth_module,
"keycloak_admin",
SimpleNamespace(ready=lambda: True, find_client=lambda _client_id: None, create_client=lambda _payload: None),
)
try:
OAuth2ProxyService().ensure_wolf()
except RuntimeError as exc:
assert "not found" in str(exc)
else:
raise AssertionError("missing client should fail")
def test_ensure_wolf_rejects_missing_client_uuid(monkeypatch) -> None:
monkeypatch.setattr(
oauth_module,
"settings",
SimpleNamespace(wolf_oidc_client_id="wolf", wolf_oidc_base_url="https://wolf.bstein.dev", wolf_oidc_vault_path="game-stream/wolf-oidc"),
)
monkeypatch.setattr(
oauth_module,
"keycloak_admin",
SimpleNamespace(ready=lambda: True, find_client=lambda _client_id: {"id": ""}, create_client=lambda _payload: None),
)
try:
OAuth2ProxyService().ensure_sunshine()
except RuntimeError as exc:
assert "id missing" in str(exc)
else:
raise AssertionError("missing client id should fail")

View File

@ -48,3 +48,26 @@ def test_from_env_includes_jenkins_weather_settings(monkeypatch) -> None:
assert cfg.testing_triage_model_url == "http://openclaw-ollama:11434"
assert cfg.testing_triage_model == "local-model"
assert cfg.testing_triage_model_timeout_sec == 33.5
def test_from_env_includes_game_stream_settings(monkeypatch) -> None:
monkeypatch.setenv("GAME_MODE_NODE_NAME", "titan-24")
monkeypatch.setenv(
"GAME_MODE_DISPLACE_WORKLOADS",
'[{"kind":"StatefulSet","namespace":"hermes","name":"hermes-llm","restoreReplicas":2}]',
)
monkeypatch.setenv("GAME_MODE_HOOK_TOKEN", "hook")
monkeypatch.setenv("WOLF_OIDC_CLIENT_ID", "wolf")
monkeypatch.setenv("WOLF_OIDC_BASE_URL", "https://wolf.bstein.dev/")
monkeypatch.setenv("WOLF_OIDC_VAULT_PATH", "game-stream/wolf-oidc")
monkeypatch.setenv("ARIADNE_SCHEDULE_WOLF_OIDC", "*/13 * * * *")
cfg = Settings.from_env()
assert cfg.game_mode_node_name == "titan-24"
assert cfg.game_mode_displace_workloads[0]["namespace"] == "hermes"
assert cfg.game_mode_hook_token == "hook"
assert cfg.wolf_oidc_client_id == "wolf"
assert cfg.wolf_oidc_base_url == "https://wolf.bstein.dev"
assert cfg.wolf_oidc_vault_path == "game-stream/wolf-oidc"
assert cfg.wolf_oidc_cron == "*/13 * * * *"

View File

@ -154,8 +154,38 @@ def test_diagnose_testing_triage_handles_disabled_and_bad_json(monkeypatch) -> N
assert "model_json_parse_failed" in diagnosis["unknowns"][0]
def test_diagnose_testing_triage_handles_empty_model_response(monkeypatch) -> None:
class EmptyResponse:
def raise_for_status(self) -> None:
return None
def json(self): # type: ignore[no-untyped-def]
return {"response": ""}
class EmptyClient:
def __init__(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def]
return None
def __enter__(self):
return self
def __exit__(self, *args) -> None: # type: ignore[no-untyped-def]
return None
def post(self, url, json=None): # type: ignore[no-untyped-def]
return EmptyResponse()
monkeypatch.setattr(testing_triage_diagnosis, "settings", SettingsStub(testing_triage_model_url="http://ollama"))
monkeypatch.setattr(testing_triage_diagnosis.httpx, "Client", EmptyClient)
diagnosis = testing_triage_diagnosis.diagnose_testing_triage({"summary": {"status": "ok", "problem_count": 0}})
assert diagnosis["status"] == "ok"
assert "empty_model_response" in diagnosis["unknowns"]
def test_latest_testing_triage_diagnosis_decodes_stored_json() -> None:
storage = DummyStorage()
assert testing_triage_diagnosis.latest_testing_triage_diagnosis(storage) is None # type: ignore[arg-type]
payload = {"kind": "testing_triage_diagnosis", "status": "ok"}
storage.events.append((testing_triage_diagnosis.TRIAGE_DIAGNOSIS_EVENT_TYPE, json.dumps(payload)))
@ -167,6 +197,9 @@ def test_latest_testing_triage_diagnosis_decodes_stored_json() -> None:
storage.events.append((testing_triage_diagnosis.TRIAGE_DIAGNOSIS_EVENT_TYPE, json.dumps(["not", "a", "dict"])))
assert testing_triage_diagnosis.latest_testing_triage_diagnosis(storage) is None # type: ignore[arg-type]
storage.events.append((testing_triage_diagnosis.TRIAGE_DIAGNOSIS_EVENT_TYPE, 7))
assert testing_triage_diagnosis.latest_testing_triage_diagnosis(storage) is None # type: ignore[arg-type]
def test_model_configuration_helpers_normalize_settings(monkeypatch) -> None:
monkeypatch.setattr(
@ -303,6 +336,33 @@ def test_diagnosis_from_model_rejects_non_english_and_out_of_scope_jobs(monkeypa
assert "model_evidence_refs_out_of_scope" in diagnosis["unknowns"]
def test_diagnosis_helpers_cover_boolean_and_defaults() -> None:
assert testing_triage_diagnosis._bool_value("yes", False) is True # noqa: SLF001
assert testing_triage_diagnosis._bool_value("no", True) is False # noqa: SLF001
assert testing_triage_diagnosis._bool_value("maybe", True) is True # noqa: SLF001
assert testing_triage_diagnosis._allowed_suite_jobs({"failed_suites": ["", "bstein_home", "data_prepper"]}) == { # noqa: SLF001
"bstein_home",
"bstein-home",
"bstein-dev-home",
"data_prepper",
"data-prepper",
}
assert testing_triage_diagnosis._default_next_actions({"problem_count": 0}) == [ # noqa: SLF001
"No action required unless a fresh bundle changes the status."
]
assert testing_triage_diagnosis._default_next_actions({"problem_count": 1}) == [ # noqa: SLF001
"Review the evidence bundle sections with non-empty problem lists.",
"Check the named Jenkins build logs and Flux Kustomizations before changing manifests.",
]
def test_safe_evidence_refs_rejects_non_ascii_refs() -> None:
unknowns = []
refs = testing_triage_diagnosis._safe_evidence_refs(["référence"], {}, unknowns) # noqa: SLF001
assert refs == []
assert unknowns == ["model_evidence_refs_non_english"]
def test_default_evidence_refs_include_failed_suites() -> None:
refs = testing_triage_diagnosis._default_evidence_refs( # noqa: SLF001
{"status": "needs_attention", "problem_count": 3, "failed_suites": ["a", "b", "c", "d", "e", "f", "g"]}

View File

@ -188,7 +188,7 @@ def test_vault_ensure_token_login(monkeypatch) -> None:
assert svc._ensure_token() == "tok"
def test_vault_read_write_kv(monkeypatch) -> None:
def test_vault_read_write_kv_secret(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
vault_addr="http://vault",
vault_token="token",
@ -197,41 +197,18 @@ def test_vault_read_write_kv(monkeypatch) -> None:
vault_k8s_token_reviewer_jwt_file="",
k8s_api_timeout_sec=5.0,
)
calls = []
monkeypatch.setattr(vault_module, "settings", dummy_settings)
calls: list[tuple[str, str, dict | None]] = []
def fake_request(self, method: str, path: str, json=None):
calls.append((method, path, json))
if method == "GET":
return DummyResponse({"data": {"data": {"client_id": "sunshine"}}})
return DummyResponse({"data": {"data": {"client_id": "wolf"}}})
return DummyResponse({})
monkeypatch.setattr(vault_module.VaultClient, "request", fake_request)
svc = VaultService()
assert svc.read_kv_secret("game-stream/sunshine-oidc") == {"client_id": "sunshine"}
svc.write_kv_secret("game-stream/sunshine-oidc", {"client_id": "sunshine"})
assert calls[-1] == (
"POST",
"/v1/kv/data/atlas/game-stream/sunshine-oidc",
{"data": {"client_id": "sunshine"}},
)
def test_vault_read_kv_missing(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
vault_addr="http://vault",
vault_token="token",
vault_k8s_role="vault",
vault_k8s_token_reviewer_jwt="jwt",
vault_k8s_token_reviewer_jwt_file="",
k8s_api_timeout_sec=5.0,
)
monkeypatch.setattr(vault_module, "settings", dummy_settings)
monkeypatch.setattr(
vault_module.VaultClient,
"request",
lambda *args, **kwargs: DummyResponse({}, status_code=404),
)
assert VaultService().read_kv_secret("missing") is None
assert svc.read_kv_secret("game-stream/wolf-oidc") == {"client_id": "wolf"}
svc.write_kv_secret("game-stream/wolf-oidc", {"client_id": "wolf"})
assert calls[-1] == ("POST", "/v1/kv/data/atlas/game-stream/wolf-oidc", {"data": {"client_id": "wolf"}})

View File

@ -0,0 +1,121 @@
from tests.unit.app.app_route_helpers import *
def test_game_stream_profile_me(monkeypatch) -> None:
ctx = AuthContext(username="brad", email="", groups=["game-stream-users"], claims={})
client = _client(monkeypatch, ctx)
resp = client.get("/api/game-stream/me", headers={"Authorization": "Bearer token"})
assert resp.status_code == 200
assert resp.json()["allowed"] is True
def test_game_mode_admin_start_and_stop(monkeypatch) -> None:
ctx = AuthContext(username="bstein", email="", groups=["admin"], claims={})
client = _client(monkeypatch, ctx)
calls = []
monkeypatch.setattr(app_module.game_mode, "start", lambda game, note=None: calls.append(("start", game, note)) or {"status": "active"})
monkeypatch.setattr(app_module.game_mode, "stop", lambda game, note=None: calls.append(("stop", game, note)) or {"status": "idle"})
monkeypatch.setattr(app_module.storage, "record_task_run", lambda *args, **kwargs: None)
monkeypatch.setattr(app_module, "_record_event", lambda *args, **kwargs: None)
start = client.post("/api/admin/game-mode/start", headers={"Authorization": "Bearer token"}, json={"game": "arc", "note": "now"})
stop = client.post("/api/admin/game-mode/stop", headers={"Authorization": "Bearer token"}, json={"game": "arc"})
assert start.status_code == 200
assert stop.status_code == 200
assert calls[0] == ("start", "arc", "now")
def test_game_mode_hook_requires_token(monkeypatch) -> None:
ctx = AuthContext(username="", email="", groups=[], claims={})
client = _client(monkeypatch, ctx)
monkeypatch.setattr(app_module, "settings", dataclasses.replace(app_module.settings, game_mode_hook_token="secret"))
resp = client.post("/api/game-mode/start", json={"game": "arc"})
assert resp.status_code == 401
def test_game_mode_hook_requires_configured_token(monkeypatch) -> None:
ctx = AuthContext(username="", email="", groups=[], claims={})
client = _client(monkeypatch, ctx)
monkeypatch.setattr(app_module, "settings", dataclasses.replace(app_module.settings, game_mode_hook_token=""))
resp = client.post("/api/game-mode/start", headers={"Authorization": "Bearer secret"}, json={"game": "arc"})
assert resp.status_code == 503
def test_game_mode_hook_start_and_stop(monkeypatch) -> None:
ctx = AuthContext(username="", email="", groups=[], claims={})
client = _client(monkeypatch, ctx)
monkeypatch.setattr(app_module, "settings", dataclasses.replace(app_module.settings, game_mode_hook_token="secret"))
monkeypatch.setattr(app_module.game_mode, "start", lambda game, note=None: {"status": "active", "game": game})
monkeypatch.setattr(app_module.game_mode, "stop", lambda game, note=None: {"status": "idle", "game": game})
monkeypatch.setattr(app_module.storage, "record_task_run", lambda *args, **kwargs: None)
monkeypatch.setattr(app_module, "_record_event", lambda *args, **kwargs: None)
start = client.post("/api/game-mode/start", headers={"Authorization": "Bearer secret"}, json={"game": "arc"})
stop = client.post("/api/game-mode/stop", headers={"x-ariadne-game-mode-token": "secret"}, json={"game": "arc"})
assert start.status_code == 200
assert stop.status_code == 200
def test_game_mode_status_error(monkeypatch) -> None:
ctx = AuthContext(username="bstein", email="", groups=["admin"], claims={})
client = _client(monkeypatch, ctx)
monkeypatch.setattr(app_module.game_mode, "status", lambda: (_ for _ in ()).throw(RuntimeError("boom")))
resp = client.get("/api/admin/game-mode/status", headers={"Authorization": "Bearer token"})
assert resp.status_code == 502
def test_game_mode_action_error_records(monkeypatch) -> None:
ctx = AuthContext(username="bstein", email="", groups=["admin"], claims={})
client = _client(monkeypatch, ctx)
recorded = []
monkeypatch.setattr(app_module.game_mode, "start", lambda *_args, **_kwargs: (_ for _ in ()).throw(RuntimeError("boom")))
monkeypatch.setattr(app_module.storage, "record_task_run", lambda *args, **kwargs: recorded.append(args))
monkeypatch.setattr(app_module, "_record_event", lambda *args, **kwargs: None)
resp = client.post("/api/admin/game-mode/start", headers={"Authorization": "Bearer token"}, json={"game": "arc"})
assert resp.status_code == 502
assert recorded
def test_wolf_oauth2_ensure(monkeypatch) -> None:
ctx = AuthContext(username="bstein", email="", groups=["admin"], claims={})
client = _client(monkeypatch, ctx)
monkeypatch.setattr(app_module.oauth2_proxy, "ensure_wolf", lambda: {"status": "ok", "client_id": "wolf"})
monkeypatch.setattr(app_module.storage, "record_task_run", lambda *args, **kwargs: None)
monkeypatch.setattr(app_module, "_record_event", lambda *args, **kwargs: None)
resp = client.post("/api/admin/game-stream/wolf/oauth2/ensure", headers={"Authorization": "Bearer token"})
assert resp.status_code == 200
assert resp.json()["client_id"] == "wolf"
def test_wolf_oauth2_ensure_error_paths(monkeypatch) -> None:
ctx = AuthContext(username="bstein", email="", groups=["admin"], claims={})
client = _client(monkeypatch, ctx)
monkeypatch.setattr(app_module.storage, "record_task_run", lambda *args, **kwargs: None)
monkeypatch.setattr(app_module, "_record_event", lambda *args, **kwargs: None)
monkeypatch.setattr(app_module.oauth2_proxy, "ensure_wolf", lambda: {"status": "error", "detail": "missing"})
resp = client.post("/api/admin/game-stream/wolf/oauth2/ensure", headers={"Authorization": "Bearer token"})
assert resp.status_code == 502
monkeypatch.setattr(app_module.oauth2_proxy, "ensure_wolf", lambda: (_ for _ in ()).throw(RuntimeError("boom")))
alias = client.post("/api/admin/game-stream/sunshine/oauth2/ensure", headers={"Authorization": "Bearer token"})
assert alias.status_code == 502
def test_record_simple_task_swallows_storage_errors(monkeypatch) -> None:
ctx = AuthContext(username="bstein", email="", groups=["admin"], claims={})
client = _client(monkeypatch, ctx)
monkeypatch.setattr(app_module.game_mode, "start", lambda game, note=None: {"status": "active", "game": game})
monkeypatch.setattr(app_module.storage, "record_task_run", lambda *args, **kwargs: (_ for _ in ()).throw(RuntimeError("db")))
monkeypatch.setattr(app_module, "_record_event", lambda *args, **kwargs: None)
resp = client.post("/api/admin/game-mode/start", headers={"Authorization": "Bearer token"}, json={"game": "arc"})
assert resp.status_code == 200

View File

@ -43,6 +43,7 @@ def test_startup_registers_metis_watch(monkeypatch) -> None:
assert any(name == "schedule.jenkins_build_weather" for name, _cron in tasks)
assert any(name == "schedule.jenkins_workspace_cleanup" for name, _cron in tasks)
assert any(name == "schedule.testing_triage" for name, _cron in tasks)
assert any(name == "schedule.wolf_oidc" for name, _cron in tasks)
def test_record_event_handles_exception(monkeypatch) -> None:
monkeypatch.setattr(app_module.storage, "record_event", lambda *args, **kwargs: (_ for _ in ()).throw(RuntimeError("fail")))

View File

@ -56,3 +56,57 @@ def test_reset_password_raises_on_error(monkeypatch) -> None:
with pytest.raises(httpx.HTTPStatusError):
client.reset_password("user", "pw", temporary=True)
def test_client_lifecycle_helpers(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
keycloak_admin_url="http://kc",
keycloak_admin_realm="atlas",
keycloak_admin_client_id="client",
keycloak_admin_client_secret="secret",
keycloak_realm="atlas",
)
monkeypatch.setattr("ariadne.services.keycloak_admin.settings", dummy_settings)
client = KeycloakAdminClient()
client._token = "token"
client._expires_at = 9999999999
dummy = DummyClient(
[
DummyResponse([{"id": "uuid", "clientId": "wolf"}]),
DummyResponse({}),
DummyResponse({}),
DummyResponse({"value": "secret"}),
DummyResponse([{"id": "scope", "name": "groups"}]),
DummyResponse({}),
]
)
monkeypatch.setattr("ariadne.services.keycloak_admin.httpx.Client", lambda *args, **kwargs: dummy)
assert client.find_client("wolf")["id"] == "uuid"
client.create_client({"clientId": "wolf"})
client.update_client("uuid", {"clientId": "wolf"})
assert client.get_client_secret("uuid") == "secret"
assert client.find_client_scope_id("groups") == "scope"
client.attach_optional_client_scope("uuid", "scope")
assert dummy.calls[-1][0] == "put"
def test_client_helpers_handle_missing_payloads(monkeypatch) -> None:
dummy_settings = types.SimpleNamespace(
keycloak_admin_url="http://kc",
keycloak_admin_realm="atlas",
keycloak_admin_client_id="client",
keycloak_admin_client_secret="secret",
keycloak_realm="atlas",
)
monkeypatch.setattr("ariadne.services.keycloak_admin.settings", dummy_settings)
client = KeycloakAdminClient()
client._token = "token"
client._expires_at = 9999999999
dummy = DummyClient([DummyResponse([]), DummyResponse([]), DummyResponse({})])
monkeypatch.setattr("ariadne.services.keycloak_admin.httpx.Client", lambda *args, **kwargs: dummy)
assert client.find_client("wolf") is None
assert client.find_client_scope_id("groups") is None
with pytest.raises(RuntimeError):
client.get_client_secret("uuid")