vaultwarden: add retry safeguards and db tuning

This commit is contained in:
Brad Stein 2026-01-18 03:00:24 -03:00
parent 343d41ecc7
commit 8b8d2c4aa8
3 changed files with 47 additions and 0 deletions

View File

@ -2,8 +2,10 @@
from __future__ import annotations from __future__ import annotations
import os
import sys import sys
import time import time
from datetime import datetime, timezone
from typing import Any, Iterable from typing import Any, Iterable
import httpx import httpx
@ -16,6 +18,8 @@ from atlas_portal.vaultwarden import invite_user
VAULTWARDEN_EMAIL_ATTR = "vaultwarden_email" VAULTWARDEN_EMAIL_ATTR = "vaultwarden_email"
VAULTWARDEN_STATUS_ATTR = "vaultwarden_status" VAULTWARDEN_STATUS_ATTR = "vaultwarden_status"
VAULTWARDEN_SYNCED_AT_ATTR = "vaultwarden_synced_at" VAULTWARDEN_SYNCED_AT_ATTR = "vaultwarden_synced_at"
VAULTWARDEN_RETRY_COOLDOWN_SEC = int(os.getenv("VAULTWARDEN_RETRY_COOLDOWN_SEC", "1800"))
VAULTWARDEN_FAILURE_BAILOUT = int(os.getenv("VAULTWARDEN_FAILURE_BAILOUT", "2"))
def _iter_keycloak_users(page_size: int = 200) -> Iterable[dict[str, Any]]: def _iter_keycloak_users(page_size: int = 200) -> Iterable[dict[str, Any]]:
@ -82,6 +86,21 @@ def _extract_attr(attrs: Any, key: str) -> str:
return "" return ""
def _parse_synced_at(value: str) -> float | None:
value = (value or "").strip()
if not value:
return None
for fmt in ("%Y-%m-%dT%H:%M:%SZ", "%Y-%m-%dT%H:%M:%S%z"):
try:
parsed = datetime.strptime(value, fmt)
if parsed.tzinfo is None:
parsed = parsed.replace(tzinfo=timezone.utc)
return parsed.timestamp()
except ValueError:
continue
return None
def _vaultwarden_email_for_user(user: dict[str, Any]) -> str: def _vaultwarden_email_for_user(user: dict[str, Any]) -> str:
username = (user.get("username") if isinstance(user.get("username"), str) else "") or "" username = (user.get("username") if isinstance(user.get("username"), str) else "") or ""
username = username.strip() username = username.strip()
@ -129,6 +148,7 @@ def main() -> int:
created = 0 created = 0
skipped = 0 skipped = 0
failures = 0 failures = 0
consecutive_failures = 0
for user in _iter_keycloak_users(): for user in _iter_keycloak_users():
username = (user.get("username") if isinstance(user.get("username"), str) else "") or "" username = (user.get("username") if isinstance(user.get("username"), str) else "") or ""
@ -158,6 +178,11 @@ def main() -> int:
current_status = _extract_attr(full_user.get("attributes"), VAULTWARDEN_STATUS_ATTR) current_status = _extract_attr(full_user.get("attributes"), VAULTWARDEN_STATUS_ATTR)
current_synced_at = _extract_attr(full_user.get("attributes"), VAULTWARDEN_SYNCED_AT_ATTR) current_synced_at = _extract_attr(full_user.get("attributes"), VAULTWARDEN_SYNCED_AT_ATTR)
current_synced_ts = _parse_synced_at(current_synced_at)
if current_status in {"rate_limited", "error"} and current_synced_ts:
if time.time() - current_synced_ts < VAULTWARDEN_RETRY_COOLDOWN_SEC:
skipped += 1
continue
email = _vaultwarden_email_for_user(full_user) email = _vaultwarden_email_for_user(full_user)
if not email: if not email:
print(f"skip {username}: missing email", file=sys.stderr) print(f"skip {username}: missing email", file=sys.stderr)
@ -188,6 +213,7 @@ def main() -> int:
result = invite_user(email) result = invite_user(email)
if result.ok: if result.ok:
created += 1 created += 1
consecutive_failures = 0
print(f"ok {username}: {result.status}") print(f"ok {username}: {result.status}")
try: try:
_set_user_attribute(username, VAULTWARDEN_STATUS_ATTR, result.status) _set_user_attribute(username, VAULTWARDEN_STATUS_ATTR, result.status)
@ -196,12 +222,17 @@ def main() -> int:
pass pass
else: else:
failures += 1 failures += 1
if result.status in {"rate_limited", "error"}:
consecutive_failures += 1
print(f"err {username}: {result.status} {result.detail}", file=sys.stderr) print(f"err {username}: {result.status} {result.detail}", file=sys.stderr)
try: try:
_set_user_attribute(username, VAULTWARDEN_STATUS_ATTR, result.status) _set_user_attribute(username, VAULTWARDEN_STATUS_ATTR, result.status)
_set_user_attribute(username, VAULTWARDEN_SYNCED_AT_ATTR, time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())) _set_user_attribute(username, VAULTWARDEN_SYNCED_AT_ATTR, time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()))
except Exception: except Exception:
pass pass
if consecutive_failures >= VAULTWARDEN_FAILURE_BAILOUT:
print("vaultwarden: too many consecutive failures; aborting run", file=sys.stderr)
break
print( print(
f"done processed={processed} created_or_present={created} skipped={skipped} failures={failures}", f"done processed={processed} created_or_present={created} skipped={skipped} failures={failures}",

View File

@ -68,6 +68,12 @@ spec:
value: bstein-dev-home-admin value: bstein-dev-home-admin
- name: HTTP_CHECK_TIMEOUT_SEC - name: HTTP_CHECK_TIMEOUT_SEC
value: "20" value: "20"
- name: VAULTWARDEN_ADMIN_SESSION_TTL_SEC
value: "900"
- name: VAULTWARDEN_RETRY_COOLDOWN_SEC
value: "1800"
- name: VAULTWARDEN_FAILURE_BAILOUT
value: "2"
volumeMounts: volumeMounts:
- name: vaultwarden-cred-sync-script - name: vaultwarden-cred-sync-script
mountPath: /scripts mountPath: /scripts

View File

@ -50,6 +50,16 @@ spec:
value: "true" value: "true"
- name: DOMAIN - name: DOMAIN
value: "https://vault.bstein.dev" value: "https://vault.bstein.dev"
- name: DB_CONNECTION_RETRIES
value: "0"
- name: DATABASE_TIMEOUT
value: "60"
- name: DATABASE_MIN_CONNS
value: "2"
- name: DATABASE_MAX_CONNS
value: "20"
- name: DATABASE_IDLE_TIMEOUT
value: "600"
- name: SMTP_HOST - name: SMTP_HOST
value: "mail.bstein.dev" value: "mail.bstein.dev"
- name: SMTP_PORT - name: SMTP_PORT