diff --git a/glance-system/glance-helper.yaml b/glance-system/glance-helper.yaml index a15f514..0145fd4 100644 --- a/glance-system/glance-helper.yaml +++ b/glance-system/glance-helper.yaml @@ -46,6 +46,8 @@ spec: value: /data - name: GLANCE_HELPER_KEY value: oplQqnLnJK2vErRVYJpvVUcSDBOSbCHZSbsYY2bwSifgTMfT + - name: TZ + value: Europe/Budapest ports: - containerPort: 8000 command: @@ -54,18 +56,18 @@ spec: args: - 'set -eux; + export DEBIAN_FRONTEND=noninteractive; + apt-get update; apt-get install -y --no-install-recommends curl ca-certificates iputils-ping - dnsutils; + dnsutils tzdata net-tools; rm -rf /var/lib/apt/lists/*; pip install --no-cache-dir fastapi uvicorn requests beautifulsoup4 prometheus-client; - python -c "import uvicorn; uvicorn.run(''app:APP'', host=''0.0.0.0'', port=8000)" - - ' + python -c "import uvicorn; uvicorn.run(''app:APP'', host=''0.0.0.0'', port=8000)"' volumeMounts: - name: app mountPath: /app @@ -183,60 +185,62 @@ data: \ media_type=\"application/json; charset=utf-8\")\n\n\n@APP.get(\"/metrics\")\n\ def metrics():\n return Response(generate_latest(), media_type=CONTENT_TYPE_LATEST)\n\ \n\n# -------------------------------\n# Tandoor helpers\n# -------------------------------\n\ - def _today_str() -> str:\n # Use Europe/Budapest for \"day\" boundaries\n \ - \ return datetime.now(tz=ZoneInfo(\"Europe/Budapest\")).date().isoformat()\n\ - \ndef _load_json(path: Path, default):\n try:\n with path.open(\"r\"\ - , encoding=\"utf-8\") as f:\n return json.load(f)\n except Exception:\n\ - \ return default\n\ndef _save_json(path: Path, data) -> None:\n tmp\ - \ = path.with_suffix(path.suffix + \".tmp\")\n with tmp.open(\"w\", encoding=\"\ - utf-8\") as f:\n json.dump(data, f, ensure_ascii=False, indent=2)\n \ - \ tmp.replace(path)\n\ndef _tandoor_headers() -> Dict[str, str]:\n token =\ - \ os.getenv(\"TANDOOR_TOKEN\", \"\")\n if not token:\n return {\"Accept\"\ - : \"application/json\"}\n return {\"Accept\": \"application/json\", \"Authorization\"\ - : f\"Bearer {token}\"}\n\ndef _rewrite_to_public(maybe_url: Optional[str]) ->\ - \ Optional[str]:\n if not maybe_url:\n return None\n\n # Relative\ - \ path -> public\n if maybe_url.startswith(\"/\"):\n return TANDOOR_PUBLIC_URL\ - \ + maybe_url\n\n # If the API returns internal host URLs, rewrite scheme+host\ - \ to public\n try:\n u = urlparse(maybe_url)\n pub = urlparse(TANDOOR_PUBLIC_URL)\n\ - \ internal = urlparse(TANDOOR_INTERNAL_URL)\n if u.netloc and internal.netloc\ - \ and u.netloc == internal.netloc:\n u = u._replace(scheme=pub.scheme,\ - \ netloc=pub.netloc)\n return urlunparse(u)\n except Exception:\n\ - \ pass\n\n return maybe_url\n\ndef _fetch_recipes_flat() -> List[Dict[str,\ - \ Any]]:\n # Prefer /api/recipe/flat/ because it's already {id,name,image}\ - \ list\n flat_url = f\"{TANDOOR_INTERNAL_URL}/api/recipe/flat/\"\n r = requests.get(flat_url,\ - \ headers=_tandoor_headers(), timeout=15)\n if r.status_code == 200:\n \ - \ data = r.json()\n # Expected: list\n if isinstance(data, list):\n\ - \ out = []\n for x in data:\n out.append({\n\ - \ \"id\": int(x.get(\"id\", 0)),\n \"name\"\ - : str(x.get(\"name\", \"\")),\n \"image\": _rewrite_to_public(x.get(\"\ - image\")),\n })\n return [x for x in out if x[\"id\"\ - ] and x[\"name\"]]\n\n # Fallback: paginated /api/recipe/\n list_url = f\"\ - {TANDOOR_INTERNAL_URL}/api/recipe/?page_size=250\"\n r = requests.get(list_url,\ - \ headers=_tandoor_headers(), timeout=15)\n r.raise_for_status()\n data\ - \ = r.json()\n items = data.get(\"results\", []) if isinstance(data, dict)\ - \ else []\n out = []\n for x in items:\n out.append({\n \ - \ \"id\": int(x.get(\"id\", 0)),\n \"name\": str(x.get(\"name\",\ - \ \"\")),\n \"image\": _rewrite_to_public(x.get(\"image\")),\n \ - \ })\n return [x for x in out if x[\"id\"] and x[\"name\"]]\n\ndef _get_cooked_for_today()\ - \ -> List[int]:\n today = _today_str()\n cooked = _load_json(COOKED_PATH,\ - \ {})\n ids = cooked.get(today, [])\n # normalize\n try:\n return\ - \ [int(i) for i in ids]\n except Exception:\n return []\n\ndef _set_cooked_today(ids:\ - \ List[int]) -> None:\n today = _today_str()\n cooked = _load_json(COOKED_PATH,\ - \ {})\n cooked[today] = sorted(list({int(i) for i in ids}))\n # Optional\ - \ cleanup: keep only last 14 days\n try:\n keys = sorted(cooked.keys())\n\ - \ if len(keys) > 14:\n for k in keys[:-14]:\n \ - \ cooked.pop(k, None)\n except Exception:\n pass\n _save_json(COOKED_PATH,\ - \ cooked)\n\ndef _get_picks_today() -> List[int]:\n today = _today_str()\n\ - \ picks = _load_json(PICKS_PATH, {})\n ids = picks.get(today, [])\n try:\n\ - \ return [int(i) for i in ids]\n except Exception:\n return []\n\ - \ndef _set_picks_today(ids: List[int]) -> None:\n today = _today_str()\n \ - \ picks = _load_json(PICKS_PATH, {})\n picks[today] = [int(i) for i in ids\ - \ if int(i) > 0]\n # cleanup old days\n try:\n keys = sorted(picks.keys())\n\ - \ if len(keys) > 14:\n for k in keys[:-14]:\n \ - \ picks.pop(k, None)\n except Exception:\n pass\n _save_json(PICKS_PATH,\ - \ picks)\n\ndef _ensure_daily_picks(recipes: List[Dict[str, Any]], count: int)\ - \ -> List[int]:\n cooked = set(_get_cooked_for_today())\n picks = _get_picks_today()\n\ - \n # Remove picks that are cooked today\n picks = [i for i in picks if i\ + def _today_str() -> str:\n # Use Europe/Budapest for \"day\" boundaries (fallback\ + \ to UTC if tzdata missing)\n try:\n return datetime.now(tz=ZoneInfo(\"\ + Europe/Budapest\")).date().isoformat()\n except Exception:\n return\ + \ datetime.utcnow().date().isoformat()\n\ndef _load_json(path: Path, default):\n\ + \ try:\n with path.open(\"r\", encoding=\"utf-8\") as f:\n \ + \ return json.load(f)\n except Exception:\n return default\n\ndef\ + \ _save_json(path: Path, data) -> None:\n tmp = path.with_suffix(path.suffix\ + \ + \".tmp\")\n with tmp.open(\"w\", encoding=\"utf-8\") as f:\n json.dump(data,\ + \ f, ensure_ascii=False, indent=2)\n tmp.replace(path)\n\ndef _tandoor_headers()\ + \ -> Dict[str, str]:\n token = os.getenv(\"TANDOOR_TOKEN\", \"\")\n if not\ + \ token:\n return {\"Accept\": \"application/json\"}\n return {\"Accept\"\ + : \"application/json\", \"Authorization\": f\"Bearer {token}\"}\n\ndef _rewrite_to_public(maybe_url:\ + \ Optional[str]) -> Optional[str]:\n if not maybe_url:\n return None\n\ + \n # Relative path -> public\n if maybe_url.startswith(\"/\"):\n \ + \ return TANDOOR_PUBLIC_URL + maybe_url\n\n # If the API returns internal host\ + \ URLs, rewrite scheme+host to public\n try:\n u = urlparse(maybe_url)\n\ + \ pub = urlparse(TANDOOR_PUBLIC_URL)\n internal = urlparse(TANDOOR_INTERNAL_URL)\n\ + \ if u.netloc and internal.netloc and u.netloc == internal.netloc:\n \ + \ u = u._replace(scheme=pub.scheme, netloc=pub.netloc)\n return\ + \ urlunparse(u)\n except Exception:\n pass\n\n return maybe_url\n\ + \ndef _fetch_recipes_flat() -> List[Dict[str, Any]]:\n # Prefer /api/recipe/flat/\ + \ because it's already {id,name,image} list\n flat_url = f\"{TANDOOR_INTERNAL_URL}/api/recipe/flat/\"\ + \n r = requests.get(flat_url, headers=_tandoor_headers(), timeout=15)\n \ + \ if r.status_code == 200:\n data = r.json()\n # Expected: list\n\ + \ if isinstance(data, list):\n out = []\n for x in\ + \ data:\n out.append({\n \"id\": int(x.get(\"\ + id\", 0)),\n \"name\": str(x.get(\"name\", \"\")),\n \ + \ \"image\": _rewrite_to_public(x.get(\"image\")),\n \ + \ })\n return [x for x in out if x[\"id\"] and x[\"name\"]]\n\n\ + \ # Fallback: paginated /api/recipe/\n list_url = f\"{TANDOOR_INTERNAL_URL}/api/recipe/?page_size=250\"\ + \n r = requests.get(list_url, headers=_tandoor_headers(), timeout=15)\n \ + \ r.raise_for_status()\n data = r.json()\n items = data.get(\"results\"\ + , []) if isinstance(data, dict) else []\n out = []\n for x in items:\n \ + \ out.append({\n \"id\": int(x.get(\"id\", 0)),\n \ + \ \"name\": str(x.get(\"name\", \"\")),\n \"image\": _rewrite_to_public(x.get(\"\ + image\")),\n })\n return [x for x in out if x[\"id\"] and x[\"name\"\ + ]]\n\ndef _get_cooked_for_today() -> List[int]:\n today = _today_str()\n \ + \ cooked = _load_json(COOKED_PATH, {})\n ids = cooked.get(today, [])\n \ + \ # normalize\n try:\n return [int(i) for i in ids]\n except Exception:\n\ + \ return []\n\ndef _set_cooked_today(ids: List[int]) -> None:\n today\ + \ = _today_str()\n cooked = _load_json(COOKED_PATH, {})\n cooked[today]\ + \ = sorted(list({int(i) for i in ids}))\n # Optional cleanup: keep only last\ + \ 14 days\n try:\n keys = sorted(cooked.keys())\n if len(keys)\ + \ > 14:\n for k in keys[:-14]:\n cooked.pop(k, None)\n\ + \ except Exception:\n pass\n _save_json(COOKED_PATH, cooked)\n\n\ + def _get_picks_today() -> List[int]:\n today = _today_str()\n picks = _load_json(PICKS_PATH,\ + \ {})\n ids = picks.get(today, [])\n try:\n return [int(i) for i\ + \ in ids]\n except Exception:\n return []\n\ndef _set_picks_today(ids:\ + \ List[int]) -> None:\n today = _today_str()\n picks = _load_json(PICKS_PATH,\ + \ {})\n picks[today] = [int(i) for i in ids if int(i) > 0]\n # cleanup old\ + \ days\n try:\n keys = sorted(picks.keys())\n if len(keys) >\ + \ 14:\n for k in keys[:-14]:\n picks.pop(k, None)\n\ + \ except Exception:\n pass\n _save_json(PICKS_PATH, picks)\n\ndef\ + \ _ensure_daily_picks(recipes: List[Dict[str, Any]], count: int) -> List[int]:\n\ + \ cooked = set(_get_cooked_for_today())\n picks = _get_picks_today()\n\n\ + \ # Remove picks that are cooked today\n picks = [i for i in picks if i\ \ not in cooked]\n\n # Top up to requested count if needed\n if len(picks)\ \ < count:\n available = [r[\"id\"] for r in recipes if r[\"id\"] not in\ \ cooked and r[\"id\"] not in picks]\n # If everything is cooked (or too\ @@ -304,26 +308,4 @@ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: glance-helper - namespace: glance-system - annotations: - cert-manager.io/cluster-issuer: letsencrypt-prod - external-dns.alpha.kubernetes.io/hostname: glance-helper.dooplex.hu,glance-helper.home - nginx.ingress.kubernetes.io/ssl-redirect: '"true"' - nginx.ingress.kubernetes.io/proxy-body-size: 10m -spec: - ingressClassName: nginx-internal - rules: - - host: glance-helper.dooplex.hu - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: glance-helper - port: - number: 8000 - tls: - - hosts: - - glance-helper.dooplex.hu - secretName: glance-helper-tls \ No newline at end of file + namespace: glance-system \ No newline at end of file