Compare commits

...
107 Commits
Author SHA1 Message Date
forust 58c8b81cce lint(postgres): 126:77 error no new line character at the end of file (new-line-at-end-of-file)
ci / lint-prettier (push) Successful in 9s
ci / lint-ruff (push) Successful in 4s
ci / lint-yaml (push) Successful in 7s
ci / lint-dockerfiles (push) Successful in 4s
ci / validate (push) Successful in 4s
ci / lint-prettier (pull_request) Successful in 8s
ci / lint-ruff (pull_request) Successful in 4s
ci / lint-yaml (pull_request) Successful in 6s
ci / lint-dockerfiles (pull_request) Successful in 4s
ci / validate (pull_request) Successful in 5s
renovate-ci / validate-renovate (pull_request) Successful in 8s
ci / build (pull_request) Has been skipped
ci / build (push) Has been skipped
ci / deploy-userbot-panel (push) Has been skipped
ci / deploy-userbot-panel (pull_request) Has been skipped
2026-09-14 13:56:06 +02:00
forust b96bdaeab7 chore(gite): updated deprecated access log config
ci / lint-prettier (push) Failing after 11s
ci / lint-ruff (push) Successful in 5s
ci / lint-yaml (push) Failing after 6s
ci / lint-dockerfiles (push) Successful in 5s
ci / validate (push) Successful in 5s
ci / lint-prettier (pull_request) Failing after 8s
ci / lint-ruff (pull_request) Successful in 4s
ci / lint-yaml (pull_request) Failing after 7s
ci / lint-dockerfiles (pull_request) Successful in 4s
ci / validate (pull_request) Successful in 5s
renovate-ci / validate-renovate (pull_request) Successful in 10s
ci / build (push) Has been skipped
ci / build (pull_request) Has been skipped
ci / deploy-userbot-panel (push) Has been skipped
ci / deploy-userbot-panel (pull_request) Has been skipped
2026-09-14 13:53:09 +02:00
forust 56f95cc377 feat(postgres): migrate gitea to shared postgres database 2026-09-14 13:52:20 +02:00
forust b4678e5437 feat(postgres): upgrade shared database to PostgreSQL 17
Move the shared postgres service from 15.19 to 17.6 as the postgres17 StatefulSet with its own PVC, extend the initdb and ingress policy with the statuspage database, and drop the now-unused per-app postgres manifests for authentik, gitea and netronome.
2026-09-14 11:54:28 +02:00
forust 18167e7bc3 ci: validate Renovate manifests with kubeconform 2026-09-14 10:51:40 +02:00
forustandCopilot 592d24ce96 feat(postgres): migrate apps to shared database
Move Authentik and Netronome to the shared PostgreSQL service after logical dump and restore.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-09-14 10:41:46 +02:00
forust fe32d0319c feat(postgres): add shared database deployments 2026-09-14 10:25:45 +02:00
forustandCopilot 24d3686f60 ci: fix formatting and YAML lint scope
ci / lint-prettier (push) Successful in 13s
ci / lint-ruff (push) Successful in 4s
ci / lint-yaml (push) Successful in 7s
ci / lint-dockerfiles (push) Successful in 5s
ci / validate (push) Successful in 6s
renovate-ci / validate-renovate (push) Failing after 2s
ci / build (push) Successful in 2s
ci / deploy-userbot-panel (push) Has been skipped
Lint tracked YAML files without scanning generated dependencies.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-09-14 09:30:55 +02:00
forust b8b3bba264 Merge branch 'feat/renovate'
ci / lint-yaml (push) Failing after 6s
ci / lint-prettier (push) Failing after 8s
ci / lint-ruff (push) Successful in 4s
ci / lint-dockerfiles (push) Successful in 4s
ci / validate (push) Successful in 5s
renovate-ci / validate-renovate (push) Failing after 2s
ci / build (push) Has been skipped
ci / deploy-userbot-panel (push) Has been skipped
2026-09-14 09:16:43 +02:00
forustandCopilot abfbc04067 fix(infra): align monitoring and Gitea database config
Keep CrowdSec scraping explicit and define Gitea's database name.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-09-14 09:16:01 +02:00
forustandCopilot 23ed72826a chore(images): pin service image updates
Replace floating service images with reviewable tags or digests.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-09-14 09:16:01 +02:00
forustandCopilot 7288058df6 feat(renovate): add Gitea update automation
Run Renovate in Kubernetes to create reviewed image update PRs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-09-14 09:16:01 +02:00
forust 6715f9e9af chore(k8s): raise resource limits for glance, edu and crowdsec 2026-09-14 01:29:28 +02:00
forust ccec1102ef ci(deploy): helm upgrade kube-prometheus-stack when active 2026-09-14 01:29:24 +02:00
forust 360a6fc5dc feat(prometheus): add alerting rules and alertmanager config 2026-09-14 01:21:27 +02:00
forust 9a806724af chore(crowdsec): remove crowdsec bouncer from dns and headscale ingresses 2026-09-14 01:15:55 +02:00
forustandCopilot ed1ddaad5d feat(crowdsec): restore web traffic protection
Protect public Traefik routes with CrowdSec HTTP decisions and restore access logging for web traffic analysis.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-09-12 21:05:12 +02:00
forustandCopilot 726b3ee544 fix(edu): run redis as statefulset
ci / deploy-userbot-panel (push) Has been skipped
ci / lint-ruff (push) Successful in 5s
ci / lint-yaml (push) Successful in 7s
ci / build (push) Successful in 3s
ci / lint-prettier (push) Successful in 11s
ci / lint-dockerfiles (push) Successful in 4s
ci / validate (push) Successful in 5s
Keep the existing Redis PVC and data while migrating the edu-master workload from Deployment to StatefulSet.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-09-10 23:35:59 +02:00
forust 13309b26e0 fix: add checkmk agent entrypoint
ci / lint-dockerfiles (push) Successful in 5s
ci / validate (push) Successful in 5s
ci / deploy-userbot-panel (push) Has been skipped
ci / lint-prettier (push) Successful in 10s
ci / lint-ruff (push) Successful in 5s
ci / lint-yaml (push) Successful in 7s
ci / build (push) Successful in 2s
update traefik to v3.7.13
2026-09-10 14:52:49 +02:00
forust eddc256bed chore: remove esp32/ingress.yaml from main
ci / lint-prettier (push) Successful in 7s
ci / lint-ruff (push) Successful in 4s
ci / deploy-userbot-panel (push) Has been skipped
ci / lint-yaml (push) Successful in 7s
ci / lint-dockerfiles (push) Successful in 5s
ci / validate (push) Successful in 5s
ci / build (push) Successful in 3s
2026-09-10 12:12:54 +02:00
forust 52f821cbba Merge branch 'main' of ssh://gitssh.forust.xyz:2221/forust/homelab
ci / lint-prettier (push) Failing after 8s
ci / lint-ruff (push) Successful in 4s
ci / lint-yaml (push) Failing after 7s
ci / lint-dockerfiles (push) Successful in 5s
ci / validate (push) Successful in 5s
ci / build (push) Has been skipped
ci / deploy-userbot-panel (push) Has been skipped
2026-09-10 11:47:25 +02:00
forust 0dbc2fff13 Merge branch 'sidetree' 2026-09-10 11:47:25 +02:00
forust 91ec83bc1c Merge branch 'main' of ssh://gitssh.forust.xyz:2221/forust/homelab
ci / lint-ruff (push) Successful in 4s
ci / lint-prettier (push) Failing after 8s
ci / lint-yaml (push) Failing after 7s
ci / lint-dockerfiles (push) Successful in 5s
ci / validate (push) Successful in 6s
ci / build (push) Has been skipped
ci / deploy-userbot-panel (push) Has been skipped
2026-09-10 11:47:20 +02:00
forust 9fec1dae39 Merge branch 'sidetree' 2026-09-10 11:47:15 +02:00
forust e5626b7b2d chore: remove untracked README.md
ci / lint-prettier (push) Successful in 7s
ci / lint-ruff (push) Successful in 5s
ci / lint-yaml (push) Successful in 6s
ci / lint-dockerfiles (push) Successful in 4s
ci / validate (push) Successful in 4s
ci / build (push) Has been skipped
ci / deploy-userbot-panel (push) Has been skipped
2026-09-10 11:45:00 +02:00
forust 8fb12a2176 chore: remove untracked README.md
ci / lint-prettier (push) Successful in 7s
ci / lint-ruff (push) Successful in 4s
ci / lint-yaml (push) Successful in 6s
ci / lint-dockerfiles (push) Successful in 4s
ci / validate (push) Successful in 5s
ci / build (push) Has been skipped
ci / deploy-userbot-panel (push) Has been skipped
2026-09-10 11:44:23 +02:00
forust fe0c7067b6 Merge branch 'main' of ssh://gitssh.forust.xyz:2221/forust/homelab
ci / validate (push) Successful in 4s
ci / build (push) Has been skipped
ci / lint-prettier (push) Failing after 8s
ci / lint-ruff (push) Successful in 7s
ci / lint-yaml (push) Failing after 6s
ci / lint-dockerfiles (push) Successful in 4s
ci / deploy-userbot-panel (push) Has been skipped
2026-09-10 11:42:17 +02:00
forust d0f0843774 Merge branch 'sidetree' 2026-09-10 11:41:35 +02:00
forust 81a5b207ac Merge branch 'main' of ssh://gitssh.forust.xyz:2221/forust/homelab
ci / lint-prettier (push) Failing after 8s
ci / lint-ruff (push) Successful in 5s
ci / lint-yaml (push) Failing after 7s
ci / lint-dockerfiles (push) Successful in 4s
ci / validate (push) Successful in 5s
ci / build (push) Has been skipped
ci / deploy-userbot-panel (push) Has been skipped
2026-09-10 11:41:31 +02:00
forust e3d5970ae3 chore: remove untracked README.md
ci / lint-prettier (push) Failing after 11s
ci / lint-ruff (push) Successful in 5s
ci / lint-yaml (push) Failing after 7s
ci / lint-dockerfiles (push) Successful in 4s
ci / validate (push) Successful in 5s
ci / build (push) Has been skipped
ci / deploy-userbot-panel (push) Has been skipped
2026-09-10 11:39:20 +02:00
forust 85f05c26cb feat(edu): activate k8s management for edu-master 2026-09-10 00:55:22 +02:00
forust 68630eb773 fix(edu): read diary event times directly from DOM, drop 25 AJAX clicks
ci / lint-prettier (push) Successful in 9s
ci / lint-ruff (push) Successful in 6s
ci / lint-yaml (push) Successful in 10s
ci / lint-dockerfiles (push) Successful in 7s
ci / validate (push) Successful in 6s
ci / build (push) Has been skipped
ci / deploy-userbot-panel (push) Has been skipped
_collect_event_times re-clicked every a.event-link and waited ~3s per
event for a visible span.data, but the calendar embeds all times in
div.event-full-info[data-event-full-info-id] span.data already. The old
loop took ~109s for 25 events and collected 0 (original divs stay
sf-hidden), effectively hanging /diary. Now a single evaluate reads all
times (~3.7s), parsing HH:MM from p.date span.data.
2026-09-10 00:31:04 +02:00
forust dad9cf2104 feat(edu): parse event times in /diary and drop weekend days
ci / lint-prettier (push) Successful in 9s
ci / lint-ruff (push) Successful in 4s
ci / lint-yaml (push) Successful in 6s
ci / lint-dockerfiles (push) Successful in 5s
ci / validate (push) Successful in 6s
ci / build (push) Has been skipped
ci / deploy-userbot-panel (push) Has been skipped
- fetch each event's time via Playwright (click event-link, read span.data, close fancybox) and render as 'title (HH:MM)'
- '08:00' placeholder renders as localized 'unknown' (time_unknown key in ru/uk/en)
- diary week view shows Mon-Fri only (title ends at Friday)
- diary month view skips Sat/Sun by weekday_idx with name-based fallback
- schedule keyboard drops Sat/Sun day buttons
2026-09-10 00:05:56 +02:00
forust 60886e8be2 style(edu): ruff-format webinar-checker/checker.py
ci / lint-prettier (push) Successful in 13s
ci / lint-ruff (push) Successful in 4s
ci / lint-yaml (push) Successful in 7s
ci / lint-dockerfiles (push) Successful in 5s
ci / validate (push) Successful in 6s
ci / build (push) Has been skipped
ci / deploy-userbot-panel (push) Has been skipped
2026-09-09 23:47:04 +02:00
forust 4528321225 fix(edu): add TZ to .env.example 2026-09-09 11:46:35 +02:00
forust b246a3dea1 fix(edu): review findings for checker.py i18n — group chat language via resolve_lang/chat: keys (default uk), weekday normalization with _norm_day + logging, drop dead translation keys, html.escape, single lang lookup, distinct whitelist emoji 2026-09-09 11:44:17 +02:00
forustandassistant 4c59a2d2fe lang(edu): translate k8s manifest comments to English only
- restore-seed-job.yaml.example: translate runbook to English
- secrets.yaml.example: translate section headers to English
- webinar-checker.yaml: translate initContainer dependency-order comments to English

Co-authored-by: assistant
2026-09-09 10:46:36 +02:00
forust fcc7b0d611 ci(deploy): gate redeploy behind manual workflow_dispatch
ci / lint-prettier (push) Successful in 8s
ci / lint-ruff (push) Successful in 4s
ci / lint-yaml (push) Successful in 7s
ci / lint-dockerfiles (push) Successful in 4s
ci / validate (push) Successful in 5s
ci / build (push) Successful in 2s
ci / deploy-userbot-panel (push) Has been skipped
removes automatic redeploy on push to main; deploy now runs only on
explicit manual trigger
2026-09-06 20:55:40 +02:00
forust 9633fe3a10 style(ci): add trailing newline to deploy workflows
ci / lint-prettier (push) Successful in 7s
ci / lint-ruff (push) Successful in 5s
ci / lint-yaml (push) Successful in 7s
ci / lint-dockerfiles (push) Successful in 5s
ci / validate (push) Successful in 5s
deploy / redeploy (push) Failing after 1s
ci / build (push) Successful in 2s
ci / deploy-userbot-panel (push) Has been skipped
2026-09-06 20:45:30 +02:00
forust d9f1c8325a feat(userbot): prereqs at startup, SPA path guard, provision lock
ci / lint-prettier (push) Failing after 8s
ci / lint-ruff (push) Successful in 4s
ci / lint-yaml (push) Failing after 7s
ci / lint-dockerfiles (push) Successful in 4s
ci / validate (push) Successful in 5s
deploy / redeploy (push) Failing after 0s
ci / build (push) Has been skipped
ci / deploy-userbot-panel (push) Has been skipped
- ensure_prerequisites runs on startup, not per-request; kube config
  errors surface as 503 PanelError
- serialize provisioning with a lock; drop per-endpoint prereq checks
- guard SPA fallback against path traversal (relative_to)
- add backend tests for auth flow, k8s service, spa routing; ci comment
  for legacy userbot deployments
2026-09-06 20:39:12 +02:00
forust 861d89d36a ci(deploy): split runtime by k8s/active marker
services marked k8s/active are applied via kubectl; the rest via docker
compose. inactive services with k8s/ keep only routing manifests
(external Services, EndpointSlices, Ingresses) to reach docker backends.
headscale/nextcloud routing moved to k8s/routing/.

validations: compose config --quiet + kubectl apply --dry-run=client.
namespace manifests applied first. pull_policy:build stacks get
build+push before up so the registry image stays fresh.
2026-09-06 20:39:12 +02:00
forust 71cddd6a91 feat(userbot): add Kubernetes control panel
Manage Telegram instances through Kubernetes with legacy adoption for forust and anna. Build and deploy the panel image alongside the runtime.
2026-09-06 20:39:12 +02:00
forust 30e6f05584 fix(edu_master): satisfy ruff in schedule scraper
- rename ambiguous loop var, merge nested if (E741, SIM102)
- use tempfile.gettempdir() for debug dump (S108)
- drop unused total_lessons assignment (F841)
2026-09-06 20:37:18 +02:00
forust bc8d74fd28 feat(schedule): per-user class picker, parser fixes
ci / lint-prettier (push) Successful in 8s
ci / lint-ruff (push) Failing after 5s
ci / lint-yaml (push) Successful in 7s
ci / lint-dockerfiles (push) Successful in 4s
ci / validate (push) Successful in 4s
ci / build (push) Has been skipped
- fix parser: capture tr attrs via finditer, strip HTML comments before
  cell parse (was leaving '-->' in subject names)
- store class choice in redis: user:{id}:schedule_class (private) and
  chat:{id}:schedule_class (groups, admin-only via /setclass)
- /schedule renders day for stored class, /setclass sets it directly
- drop teacher emoji, format grade as "N клас"
2026-09-06 16:17:48 +02:00
forust d2d4efb0d7 feat: add schedule scraper for lessons table
ci / lint-prettier (push) Successful in 9s
ci / lint-ruff (push) Failing after 4s
ci / lint-yaml (push) Successful in 7s
ci / lint-dockerfiles (push) Successful in 6s
ci / validate (push) Successful in 7s
ci / build (push) Has been skipped
Add /schedule command to scrape edu.edu.vn.ua/lessons/table via Playwright.
Inline keyboard flow: pick weekday (with today/tomorrow shortcuts),
then pick class. Cache 5h per user. Parse subjects/notes/teachers,
multi-lesson cells (hr-separated).
2026-09-06 15:46:41 +02:00
forust b752bf88bd feat: add edu_master k8s manifests for k0s migration
ci / lint-prettier (push) Successful in 10s
ci / lint-ruff (push) Successful in 4s
ci / lint-yaml (push) Successful in 7s
ci / lint-dockerfiles (push) Successful in 5s
ci / validate (push) Successful in 5s
ci / build (push) Successful in 35s
2026-09-06 15:00:51 +02:00
forust 587611ca88 chore: remove empty middlewares blocks from k8s ingresses 2026-09-02 12:17:04 +02:00
forust ace23ad1f9 fix: remove www.xdfnx.cfd from ingress, add Gitea access log config
ci / lint-prettier (push) Successful in 13s
ci / lint-ruff (push) Successful in 7s
ci / lint-yaml (push) Successful in 17s
ci / lint-dockerfiles (push) Successful in 14s
ci / validate (push) Successful in 15s
ci / build (push) Successful in 26s
2026-07-20 11:40:41 +02:00
forust 92aa731e44 deleted crowdsec stack from the repo. will figure something else
ci / lint-prettier (push) Successful in 18s
ci / lint-ruff (push) Successful in 6s
ci / lint-yaml (push) Successful in 6s
ci / lint-dockerfiles (push) Successful in 5s
ci / validate (push) Successful in 6s
ci / build (push) Successful in 43s
Signed-off-by: mr-forust <vzlomdsisma@gmail.com>
2026-07-19 23:16:14 +02:00
forust 87ca3fd40c chore: updatet chernuha's pfp, added projects sections for forust.xyz
ci / lint-prettier (push) Successful in 8s
ci / lint-ruff (push) Successful in 3s
ci / lint-yaml (push) Successful in 7s
ci / lint-dockerfiles (push) Successful in 6s
ci / validate (push) Successful in 7s
ci / build (push) Successful in 18s
2026-07-05 02:59:31 +02:00
forust 82bcd30ed8 updated xdfnx's page
ci / lint-ruff (push) Successful in 4s
ci / lint-yaml (push) Successful in 6s
ci / lint-prettier (push) Successful in 8s
ci / lint-dockerfiles (push) Successful in 4s
ci / validate (push) Successful in 11s
ci / build (push) Successful in 19s
2026-07-04 01:01:05 +02:00
forust 620262d98c ci: fetch full history for change detection after rebase
ci / lint-prettier (push) Successful in 9s
ci / lint-ruff (push) Successful in 6s
ci / lint-yaml (push) Successful in 9s
ci / build (push) Successful in 3s
ci / lint-dockerfiles (push) Successful in 5s
ci / validate (push) Successful in 5s
2026-07-04 00:55:46 +02:00
forust 831f3a46b0 updated xdfnx's page
ci / lint-prettier (push) Successful in 8s
ci / lint-ruff (push) Successful in 4s
ci / lint-yaml (push) Successful in 6s
ci / lint-dockerfiles (push) Successful in 5s
ci / validate (push) Successful in 5s
ci / build (push) Successful in 3s
2026-07-04 00:46:13 +02:00
forust f7902e74e8 ci: require lint and validate before build
ci / lint-ruff (push) Successful in 4s
ci / lint-yaml (push) Successful in 6s
ci / validate (push) Successful in 5s
ci / build (push) Successful in 2s
ci / lint-prettier (push) Successful in 7s
ci / lint-dockerfiles (push) Successful in 3s
2026-06-30 01:23:53 +02:00
forust eb8d1b361e ci: add branch tags to docker builds
ci / lint (push) Successful in 15s
ci / validate (push) Successful in 8s
ci / build (push) Successful in 3s
2026-06-30 01:20:51 +02:00
forust 6e2cafb206 ci: merge lint validate and docker builds
ci / lint (push) Successful in 14s
ci / validate (push) Successful in 19s
ci / build (push) Successful in 2s
2026-06-30 01:17:31 +02:00
forust 67b0c0824f ci: restore docker-based lint and validate jobs
lint / prettier (push) Successful in 7s
lint / ruff (push) Successful in 3s
lint / yamllint (push) Successful in 5s
lint / hadolint (push) Successful in 4s
validate / yaml (push) Successful in 6s
validate / k8s (push) Successful in 4s
2026-06-30 01:12:23 +02:00
forust 8e72e0a920 ci: run lint and validate tools locally
lint / prettier (push) Failing after 2s
lint / ruff (push) Successful in 2s
lint / yamllint (push) Successful in 3s
lint / hadolint (push) Failing after 2s
validate / yaml (push) Successful in 2s
validate / k8s (push) Failing after 1s
2026-06-30 00:14:14 +02:00
forust 73a1132beb Align Traefik Helm log values with chart v41
lint / prettier (push) Successful in 1m59s
lint / ruff (push) Successful in 4s
lint / yamllint (push) Successful in 5s
lint / hadolint (push) Successful in 4s
validate / yaml (push) Successful in 6s
validate / k8s (push) Successful in 3s
2026-06-29 23:02:26 +02:00
forust a128523c24 Fix CrowdSec middleware references in Traefik ingresses 2026-06-29 22:47:21 +02:00
forust ee881acd0e refactor: update xdfnx homepage index.html
lint / prettier (push) Successful in 14s
lint / ruff (push) Successful in 5s
lint / yamllint (push) Successful in 9s
lint / hadolint (push) Successful in 17s
validate / yaml (push) Successful in 8s
validate / k8s (push) Successful in 7s
2026-06-28 11:59:57 +02:00
forust bacb2f4b9f fix: correct Traefik rule syntax for local IngressRoutes
Move parentheses outside Host() calls so that || and && operators
are properly grouped in Traefik rule expressions.
2026-06-28 11:59:52 +02:00
forust 91211e7b78 fix: resolve CrowdSec Helm upgrade failure - remove duplicate DISABLE_ONLINE_API env and add metrics config 2026-06-28 11:59:46 +02:00
forust 9b3a7aadb4 fix: resolve pyrogram imports by adding venv and pyright config
lint / prettier (push) Successful in 7s
lint / ruff (push) Successful in 4s
lint / yamllint (push) Successful in 6s
lint / hadolint (push) Successful in 4s
validate / yaml (push) Successful in 5s
validate / k8s (push) Successful in 5s
- Create .venv with pyrofork and all dependencies installed
- Add pyrightconfig.json at workspace root and in userbot/
- Enable pyright as language server for Python in Zed settings
- Update uv.lock with resolved dependency tree
2026-06-21 22:33:04 +02:00
forust b123621ead chore: remove github obsolete prod deploy workflow
lint / prettier (push) Successful in 7s
lint / ruff (push) Successful in 4s
lint / yamllint (push) Successful in 6s
lint / hadolint (push) Successful in 4s
validate / yaml (push) Successful in 6s
validate / k8s (push) Successful in 4s
2026-06-21 22:24:30 +02:00
forust a2df8504f5 Fix DL3013: pin pip version in webinar-checker Dockerfile
lint / prettier (push) Successful in 8s
lint / ruff (push) Successful in 4s
lint / yamllint (push) Successful in 6s
lint / hadolint (push) Successful in 4s
validate / yaml (push) Successful in 6s
validate / k8s (push) Successful in 4s
2026-06-21 22:07:44 +02:00
forust fb43306571 Fix all lint issues: Dockerfiles (DL3015/DL3013/DL4006) + Ruff (173→0 errors)
lint / prettier (push) Successful in 8s
lint / ruff (push) Successful in 5s
lint / yamllint (push) Successful in 7s
lint / hadolint (push) Failing after 4s
validate / yaml (push) Successful in 5s
validate / k8s (push) Successful in 5s
Dockerfile fixes:
- edu_master/phpsessid-bot: add --no-install-recommends, pin pip versions
- edu_master/webinar-checker: pin pip versions with --no-cache-dir
- userbot: add SHELL with pipefail for pipe operations

Ruff fixes (173 → 0):
- W293/W291/W292: whitespace clean via ruff format
- N806: camelCase → snake_case (anilist, safone, hearts, flux, etc.)
- ARG001/ARG002: prefix unused params with _
- SIM115: use context managers for file I/O
- SIM117: combine nested with statements
- S608: noqa on SQL f-strings (module name is validated)
- E402/N812/N817: import fixes
- B023: pass loop variable as argument
- I001: auto-sorted imports
- syntax: fixed = vs == in dtek_notif/main.py
2026-06-21 22:01:51 +02:00
forust f424d91405 Tighten lint workflow scope
lint / prettier (push) Successful in 8s
lint / ruff (push) Failing after 5s
lint / yamllint (push) Successful in 6s
lint / hadolint (push) Failing after 4s
validate / yaml (push) Successful in 6s
validate / k8s (push) Successful in 4s
2026-06-21 21:51:20 +02:00
forust 88a8f2987f Unify workflow job naming
lint / prettier (push) Failing after 9s
lint / ruff (push) Failing after 5s
lint / yamllint (push) Successful in 7s
lint / hadolint (push) Failing after 13s
validate / yaml (push) Successful in 6s
validate / k8s (push) Successful in 5s
2026-06-21 21:46:22 +02:00
forust 17027b232b Rename workflows to yaml 2026-06-21 21:45:16 +02:00
forust 6ecbbb39b4 Fix yamllint workflow invocation
validate / k8s (push) Successful in 5s
validate / yaml (push) Successful in 7s
2026-06-21 21:40:11 +02:00
forust 175cbc8860 Fix validation workflows for self-hosted runner
validate / yaml (push) Failing after 15s
validate / k8s (push) Successful in 9s
2026-06-21 21:37:30 +02:00
forust 6363d050b0 Add YAML validation workflows 2026-06-21 21:27:31 +02:00
forust c855764bc6 feat: add vaultwarden deployment config (compose + k8s)
Deploy to Server / deploy (push) Failing after 1s
2026-06-21 01:02:09 +02:00
forust 7d92b85e21 Merge commit '4355f451d4b4288d43468e08cad1f377e511f98a'
Deploy to Server / deploy (push) Has been cancelled
2026-06-19 23:23:28 +02:00
forust 9364392bc0 Merge commit '3eaef5dc90b716cc0fa391cbe2394be56d5f6041' as 'userbot'
Deploy to Server / deploy (push) Has been cancelled
2026-06-19 23:20:23 +02:00
forust 4355f451d4 Merge commit '3eaef5dc90b716cc0fa391cbe2394be56d5f6041' as 'userbot' 2026-06-19 23:20:23 +02:00
forust 3eaef5dc90 Squashed 'userbot/' content from commit 7fb0a0e
git-subtree-dir: userbot
git-subtree-split: 7fb0a0e179
2026-06-19 23:20:23 +02:00
forust 69ffd3682e refactor: userbot to subtree 2026-06-19 23:20:13 +02:00
forust 1930600c40 Revert "refactor: extract userbot to standalone repo, add as git submodule"
This reverts commit 3c383db9a7.
2026-06-19 23:11:27 +02:00
forust d74a705d53 chore(k8s): rewritten ingressroute to include headplane AND headscale-admin ui
Deploy to Server / deploy (push) Has been cancelled
new routes groupping style
2026-06-19 23:02:22 +02:00
forust 3c383db9a7 refactor: extract userbot to standalone repo, add as git submodule
Deploy to Server / deploy (push) Has been cancelled
userbot now lives at ssh://git@gitssh.forust.xyz:2221/forust/userbot.git
and is included in homelab as a submodule at userbot/
2026-06-19 15:39:00 +02:00
forust 7fb0a0e179 chore: batch lint fixes across userbot and edu_master
- S113: Add timeout=10 to all requests calls (74 fixes)
- E722: Replace bare except: with except Exception:
- B904: Replace redundant re-raise with bare raise
- E402: Add noqa for intentional late imports after import_library()
- S102/S307/S310/S311/S603/S605/S606/S607/S108: Add noqa for intentional usage
- F601: Fix duplicate dict key in unsplash.py
- N802: Rename ReplyCheck -> reply_check with backward compat alias
- N813: Rename bs -> BS in icons.py
- B007/B020: Rename loop var _j in animations.py
- SIM102: Collapse nested if in autofwd.py
- SIM113: Use enumerate() in calculator.py
- A002: Add noqa for builtin shadowing in admlist.py
- F811: Add noqa for cohere redefinition
- edu_master: Fix ARG001, S108, S110, SIM117, apply --unsafe-fixes
- Add modules_list.txt with full module inventory
2026-06-19 15:36:25 +02:00
forust 227e5fda27 chore: batch lint fixes across userbot and edu_master
- S113: Add timeout=10 to all requests calls (74 fixes)
- E722: Replace bare except: with except Exception:
- B904: Replace redundant re-raise with bare raise
- E402: Add noqa for intentional late imports after import_library()
- S102/S307/S310/S311/S603/S605/S606/S607/S108: Add noqa for intentional usage
- F601: Fix duplicate dict key in unsplash.py
- N802: Rename ReplyCheck -> reply_check with backward compat alias
- N813: Rename bs -> BS in icons.py
- B007/B020: Rename loop var _j in animations.py
- SIM102: Collapse nested if in autofwd.py
- SIM113: Use enumerate() in calculator.py
- A002: Add noqa for builtin shadowing in admlist.py
- F811: Add noqa for cohere redefinition
- edu_master: Fix ARG001, S108, S110, SIM117, apply --unsafe-fixes
- Add modules_list.txt with full module inventory
2026-06-19 15:36:25 +02:00
forust 20bce5b31c fix(userbot): add missing safone.py imports, apply ruff --unsafe-fixes
- Add missing aiohttp, PIL, BytesIO imports to safone.py (F821 runtime bugs)
- Apply ruff --unsafe-fixes (27 fixes): ternary operators, .get() patterns,
  contextlib.suppress, enumerate(), collapsible if/else, remove .keys()
2026-06-19 15:27:12 +02:00
forust 70d7855f06 fix(userbot): add missing safone.py imports, apply ruff --unsafe-fixes
- Add missing aiohttp, PIL, BytesIO imports to safone.py (F821 runtime bugs)
- Apply ruff --unsafe-fixes (27 fixes): ternary operators, .get() patterns,
  contextlib.suppress, enumerate(), collapsible if/else, remove .keys()
2026-06-19 15:27:12 +02:00
forust c905bbd039 chore(userbot): optimize Dockerfile with multi-stage build and static ffmpeg
- Multi-stage build: pip deps built in separate stage
- Static ffmpeg binary instead of apt package (avoid 200+ deps)
- Keep only git, mediainfo, wget via apt
2026-06-19 14:56:37 +02:00
forust 7ba6bc44f2 chore(userbot): apply ruff check --fix and ruff format
- ruff check --fix: 210 auto-fixed errors (import sorting, trailing
  whitespace, unused imports, f-string fixups, deprecated annotations)
- ruff format: 104 files reformatted to consistent style
- 268 non-auto-fixable issues remain (S113 requests timeout, etc.)
2026-06-19 12:19:01 +02:00
forust 95cec59263 chore(userbot): apply prettier formatting across manifests 2026-06-19 12:03:13 +02:00
forust 20b8c93275 chore(k8s): make dockmon statefulset
+ adjusted userbot sys reqs
2026-06-10 19:40:45 +02:00
forust bb821e138a chore(k8): adjusted system resources requests and limits based on manual monitoring 2026-06-10 19:37:16 +02:00
forust b7854447af lint: yaml spaces and tabs 2026-06-09 12:59:36 +02:00
forust 444bb97f8e feat: add userbot k8s deployment method
- Kustomize: base + overlays/dev + overlays/prod
2026-06-07 19:41:28 +02:00
forust 4f01cdac31 fix: removed git checkout (was destructive)
- Remove git init/fetch/checkout from utils/misc.py
- Hardcode userbot_version to 2.5.0
2026-06-07 19:40:52 +02:00
forust cb40b10ecf chore: add gitea container registry compose support for localy builded apps 2026-06-07 14:37:57 +02:00
forust bed58c84ef refactor: update .dockerignore
pull_policy never to use local images
2026-05-25 01:43:46 +02:00
forust 56e5d79e3e refactor: improved error handling, timeouts and use temp files 2026-05-21 22:14:29 +02:00
forust cd0ce06246 refactor: imporved layer caching for dockerfile
using existing built image for account 2
2026-05-21 22:12:33 +02:00
forust a699ceb935 refactor: userbots' compose-files cleanup 2025-12-09 21:45:24 +01:00
forust ce66a546f1 chore: update gitignore, add translations 2025-11-27 18:58:17 +01:00
forust a30bda4940 Renamed userbot's dockercompose for easy access 2025-11-13 04:00:23 +01:00
forust b722467991 Remove unnecessary network and development configurations from userbot Docker Compose file 2025-11-13 03:48:50 +01:00
forust 5f8fd05266 Add userbot Docker Compose configuration for userbot_forust and userbot_anna services 2025-11-13 00:17:26 +01:00
forust 1c7afe5bb3 Move userbot Docker Compose configuration for forust and anna services 2025-11-11 01:57:44 +01:00
forust 4ff80c07b0 deleting that install shit 2025-11-11 01:41:24 +01:00
forust 3a5652daa7 userbot/install.sh, start 2025-11-11 01:39:53 +01:00
forust 4e18cd0eb3 init, .gitignore 2025-11-11 00:02:49 +01:00
266 changed files with 14402 additions and 3049 deletions

No files matched your search

+191
View File
@@ -0,0 +1,191 @@
# Инструкция: Анализ хранилища Kubernetes и настройка NFS
## Цель
Проанализировать текущую конфигурацию хранилища Kubernetes и подготовить план внедрения NFS StorageClass для сохранения данных при удалении namespace.
## 1. Собрать информацию о кластере
### 1.1. Версия Kubernetes и тип дистрибутива
```bash
kubectl version --short
# или
kubectl version
```
Определить, используется ли k3s, k8s, microk8s и т.д.:
```bash
# Проверить наличие k3s
which k3s
# Проверить процесс
ps aux | grep -E 'kube|k3s'
```
### 1.2. StorageClass
```bash
kubectl get storageclass -o wide
```
Запомнить:
- `PROVISIONER` — какой драйвер используется
- `RECLAIMPOLICY` — Delete или Retain
- Какой StorageClass помечен как `(default)`
### 1.3. Существующие PV и PVC
```bash
kubectl get pv -o wide
kubectl get pvc --all-namespaces
```
Посмотреть, какие PVC привязаны к каким PV, и какой reclaimPolicy у PV.
### 1.4. Нода и диски
```bash
# Список нод
kubectl get nodes -o wide
# На каждой ноде (через ssh или локально):
lsblk
df -h
cat /etc/fstab
```
Определить:
- Есть ли отдельный раздел/диск для данных
- Куда смонтированы разделы
- Сколько свободного места
- Есть ли монтирование NTFS-разделов (как `/media/forust/Programs`)
### 1.5. Где local-path хранит данные (для k3s)
```bash
ls -la /var/lib/rancher/k3s/storage/ 2>/dev/null
# или для microk8s
ls -la /var/snap/microk8s/common/ 2>/dev/null
```
## 2. Анализ: сохраняются ли данные при удалении namespace?
| Сценарий | Результат |
|---|---|
| `kubectl delete ns <ns>` | Все PVC в namespace удаляются |
| PVC → PV c `reclaimPolicy: Delete` | PV и данные удалены |
| PVC → PV c `reclaimPolicy: Retain` | PV остаётся (статус Released), данные целы |
**Вывод:** Если reclaimPolicy в StorageClass = `Delete`, то данные **пропадут**. Если `Retain` — сохранятся.
## 3. План внедрения NFS
### 3.1. Проверить, установлен ли NFS
```bash
which nfsstat exportfs mount.nfs
systemctl status nfs-server 2>/dev/null || systemctl status nfs-kernel-server 2>/dev/null
```
### 3.2. Выбрать директорию для NFS-экспорта
Варианты (выбрать подходящий):
- `/var/lib/k8s-nfs/` — на корневом разделе
- `<путь к отдельному разделу>/k8s-nfs/` — если есть отдельный диск/раздел
- Не рекомендуется использовать NTFS-раздел (проблемы с правами и производительностью)
Требования:
- Файловая система: ext4 или xfs (не ntfs!)
- Достаточно свободного места
- Права: `755`, владелец root
### 3.3. Установить NFS-сервер
```bash
# Debian/Ubuntu
apt update && apt install -y nfs-kernel-server
# RHEL/Fedora
dnf install -y nfs-utils
```
### 3.4. Настроить экспорт
Создать директорию:
```bash
mkdir -p /var/lib/k8s-nfs
chmod 755 /var/lib/k8s-nfs
```
Добавить в `/etc/exports`:
```
/var/lib/k8s-nfs *(rw,sync,no_subtree_check,no_root_squash)
```
Применить:
```bash
exportfs -rav
```
Проверить:
```bash
showmount -e localhost
```
### 3.5. Выбрать способ интеграции с Kubernetes
#### Вариант A: nfs-subdir-external-provisioner (проще)
```bash
helm repo add nfs-subdir-external-provisioner https://kubernetes-sigs.github.io/nfs-subdir-external-provisioner/
helm install nfs-provisioner nfs-subdir-external-provisioner/nfs-subdir-external-provisioner \
--namespace kube-system \
--set nfs.server=127.0.0.1 \
--set nfs.path=/var/lib/k8s-nfs \
--set storageClass.name=nfs \
--set storageClass.defaultClass=false \
--set storageClass.reclaimPolicy=Retain
```
#### Вариант B: NFS CSI Driver
```bash
helm repo add csi-driver-nfs https://raw.githubusercontent.com/kubernetes-csi/csi-driver-nfs/master/charts
helm install csi-driver-nfs csi-driver-nfs/csi-driver-nfs --namespace kube-system
```
После установки CSI драйвера создать StorageClass:
```yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: nfs
provisioner: nfs.csi.k8s.io
parameters:
server: 127.0.0.1
share: /var/lib/k8s-nfs
reclaimPolicy: Retain
volumeBindingMode: Immediate
```
### 3.6. Проверить результат
```bash
kubectl get storageclass
kubectl get pods -n kube-system | grep -E 'nfs|provisioner'
```
## 4. Итоговая конфигурация
После внедрения в кластере будет два StorageClass:
| Имя | Provisioner | ReclaimPolicy | Назначение |
|---|---|---|---|
| `local-path` (default) | rancher.io/local-path | Delete | Временные данные, stateless |
| `nfs` | nfs-subdir-external-provisioner или nfs.csi.k8s.io | Retain | Данные, которые нужно сохранять |
**Главное преимущество:** PVC c `storageClassName: nfs` при удалении namespace сохраняют данные на диске, так как NFS-провизор использует `reclaimPolicy: Retain` или файлы физически остаются в NFS-экспорте.
## 5. Ответы на частые вопросы
**В:** Не упадёт ли local-path при установке NFS?
**О:** Нет, они независимы. local-path продолжает работать как обычно.
**В:** Данные NFS и local-path будут на одном диске?
**О:** Да, можно настроить оба на одном разделе, в разных каталогах.
**В:** Что если у меня несколько нод?
**О:** NFS сервер нужно поднять на одной ноде, а с других нод должна быть доступна шари. Для multi-node лучше использовать отдельный сервер или distributed storage (Longhorn, Rook/Ceph).
**В:** Можно ли использовать существующий NTFS-раздел для NFS?
**О:** Не рекомендуется — NTFS не поддерживает права Linux (no_root_squash не сработает корректно), возможны проблемы с блокировками и производительностью.
+370
View File
@@ -0,0 +1,370 @@
name: ci
on:
push:
branches:
- "**"
pull_request:
workflow_dispatch:
env:
REGISTRY: gcr.forust.xyz
jobs:
lint-prettier:
runs-on: [self-hosted, linux, arch, homelab]
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Check formatting with Prettier
shell: bash
run: |
mapfile -t prettier_files < <(
git ls-files \
| grep -E '\.(md|json|ya?ml|html|css)$' \
| grep -Ev '^(\.docs/|\.zed/|errorpages/html/|homepages/(forust_files|xdfnx_files)/)'
)
if [ "${#prettier_files[@]}" -eq 0 ]; then
echo "No Prettier-managed files found."
exit 0
fi
docker run --rm \
-v "$PWD:/work" \
-w /work \
node:22-alpine \
sh -lc 'npx --yes prettier@3 --check --ignore-unknown "$@"' sh "${prettier_files[@]}"
lint-ruff:
runs-on: [self-hosted, linux, arch, homelab]
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Lint Python with Ruff
shell: bash
run: |
docker run --rm \
-v "$PWD:/work" \
-w /work \
ghcr.io/astral-sh/ruff:latest \
check .
lint-yaml:
runs-on: [self-hosted, linux, arch, homelab]
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Lint YAML syntax
shell: bash
run: |
mapfile -t yaml_files < <(
git ls-files '*.yaml' '*.yml' \
':!node_modules/**' \
':!**/.venv/**'
)
if [ "${#yaml_files[@]}" -eq 0 ]; then
echo "No YAML files found."
exit 0
fi
docker run --rm \
-v "$PWD:/work" \
-w /work \
cytopia/yamllint:latest \
-c .yamllint "${yaml_files[@]}"
lint-dockerfiles:
runs-on: [self-hosted, linux, arch, homelab]
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Lint Dockerfiles
shell: bash
run: |
mapfile -t dockerfiles < <(
git ls-files ':(glob)**/Dockerfile' ':(glob)**/Dockerfile.*'
)
if [ "${#dockerfiles[@]}" -eq 0 ]; then
echo "No Dockerfiles found."
exit 0
fi
docker run --rm \
-v "$PWD:/work" \
-w /work \
--entrypoint hadolint \
hadolint/hadolint:latest-debian \
-c .hadolint.yaml "${dockerfiles[@]}"
validate:
runs-on: [self-hosted, linux, arch, homelab]
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Validate Kubernetes manifests
shell: bash
run: |
mapfile -t manifests < <(
git ls-files ':(glob)**/k8s/**/*.yaml' ':(glob)**/k8s/**/*.yml' \
| grep -Ev '(^|/)(kustomization\.ya?ml|.*\.example\.ya?ml|.*values\.ya?ml|patch-.*\.ya?ml)$'
)
if [ "${#manifests[@]}" -eq 0 ]; then
echo "No Kubernetes manifests found."
exit 0
fi
docker run --rm \
-v "$PWD:/work" \
-w /work \
ghcr.io/yannh/kubeconform:latest \
-strict \
-ignore-missing-schemas \
-summary \
"${manifests[@]}"
build:
needs: [lint-prettier, lint-ruff, lint-yaml, lint-dockerfiles, validate]
if: github.event_name != 'pull_request' && (github.ref_name == 'main' || github.ref_name == 'dev')
runs-on: [self-hosted, linux, arch, homelab]
outputs:
services: ${{ steps.services.outputs.services }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Detect changed docker-built services
id: services
shell: bash
run: |
base="${{ github.event.before }}"
if [ -z "$base" ] || [ "$base" = "0000000000000000000000000000000000000000" ]; then
base="$(git rev-list --max-parents=0 HEAD)"
fi
mapfile -t changed_files < <(git diff --name-only "$base" "${GITHUB_SHA}")
services=()
add_service() {
local name="$1"
local seen=0
for existing in "${services[@]}"; do
if [ "$existing" = "$name" ]; then
seen=1
break
fi
done
if [ "$seen" -eq 0 ]; then
services+=("$name")
fi
}
for file in "${changed_files[@]}"; do
case "$file" in
dtek_notif/*)
add_service dtek_notif
;;
errorpages/*)
add_service errorpages
;;
userbot/*)
add_service userbot
;;
homepages/*)
add_service homepages
;;
edu_master/phpsessid-bot/*|edu_master/webinar-checker/*|edu_master/compose.yaml)
add_service edu_master
;;
esac
done
if [ "${#services[@]}" -eq 0 ]; then
echo "No docker-built services changed."
echo "services=" >> "$GITHUB_OUTPUT"
exit 0
fi
printf '%s\n' "${services[@]}" | tee /tmp/services.txt
echo "services=$(paste -sd, /tmp/services.txt)" >> "$GITHUB_OUTPUT"
- name: Log in to registry
if: steps.services.outputs.services != ''
shell: bash
run: |
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login "${REGISTRY}" \
-u "${{ secrets.REGISTRY_USERNAME }}" \
--password-stdin
- name: Build and push changed images
if: steps.services.outputs.services != ''
shell: bash
run: |
IFS=, read -r -a services <<< "${{ steps.services.outputs.services }}"
for service in "${services[@]}"; do
case "$service" in
dtek_notif)
image="${REGISTRY}/forust/dtek-notif"
tags=("latest")
case "${GITHUB_REF_NAME}" in
main)
tags+=("main" "prod")
;;
dev)
tags+=("dev")
;;
esac
build_args=()
for tag in "${tags[@]}"; do
build_args+=(-t "${image}:${tag}")
done
docker build "${build_args[@]}" dtek_notif
for tag in "${tags[@]}"; do
docker push "${image}:${tag}"
done
;;
errorpages)
image="${REGISTRY}/forust/error-pages"
tags=("latest")
case "${GITHUB_REF_NAME}" in
main)
tags+=("main" "prod")
;;
dev)
tags+=("dev")
;;
esac
build_args=()
for tag in "${tags[@]}"; do
build_args+=(-t "${image}:${tag}")
done
docker build "${build_args[@]}" errorpages
for tag in "${tags[@]}"; do
docker push "${image}:${tag}"
done
;;
userbot)
tags=("latest")
case "${GITHUB_REF_NAME}" in
main)
tags+=("main" "prod")
;;
dev)
tags+=("dev")
;;
esac
for target in runtime panel; do
case "$target" in
runtime)
context="userbot"
image="${REGISTRY}/forust/userbot"
;;
panel)
context="userbot/panel"
image="${REGISTRY}/forust/userbot-panel"
;;
esac
build_args=()
for tag in "${tags[@]}"; do
build_args+=(-t "${image}:${tag}")
done
docker build "${build_args[@]}" "$context"
for tag in "${tags[@]}"; do
docker push "${image}:${tag}"
done
done
;;
homepages)
for service in forust xdfnx; do
case "$service" in
forust)
image="${REGISTRY}/forust/forust-homepage"
;;
xdfnx)
image="${REGISTRY}/forust/xdfnx-homepage"
;;
esac
tags=("latest")
case "${GITHUB_REF_NAME}" in
main)
tags+=("main" "prod")
;;
dev)
tags+=("dev")
;;
esac
build_args=()
for tag in "${tags[@]}"; do
build_args+=(-t "${image}:${tag}")
done
docker build "${build_args[@]}" -f "homepages/Dockerfile.${service}" homepages
for tag in "${tags[@]}"; do
docker push "${image}:${tag}"
done
done
;;
edu_master)
for service in session-keeper webinar-checker; do
case "$service" in
session-keeper)
context="edu_master/phpsessid-bot"
image="${REGISTRY}/forust/session-keeper"
;;
webinar-checker)
context="edu_master/webinar-checker"
image="${REGISTRY}/forust/webinar-checker"
;;
esac
tags=("latest")
case "${GITHUB_REF_NAME}" in
main)
tags+=("main" "prod")
;;
dev)
tags+=("dev")
;;
esac
build_args=()
for tag in "${tags[@]}"; do
build_args+=(-t "${image}:${tag}")
done
docker build "${build_args[@]}" "$context"
for tag in "${tags[@]}"; do
docker push "${image}:${tag}"
done
done
;;
esac
done
deploy-userbot-panel:
needs: build
if: github.ref_name == 'main' && contains(needs.build.outputs.services, 'userbot')
runs-on: [self-hosted, linux, arch, homelab, prod]
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Apply and roll out userbot panel
shell: bash
run: |
kubectl apply -f userbot/k8s/base/panel.yaml
kubectl get secret userbot-common-secrets -n default -o json \
| jq 'del(.metadata.annotations,.metadata.creationTimestamp,.metadata.resourceVersion,.metadata.uid,.metadata.managedFields) | .metadata.namespace = "userbot"' \
| kubectl apply -f -
# Keep legacy deployments (forust/anna) in sync with manifests; they have no replicas field, so apply leaves scaling to the user manager only.
kubectl apply -f userbot/k8s/base/userbots.yaml
kubectl rollout restart deployment/userbot-panel -n userbot
kubectl rollout status deployment/userbot-panel -n userbot --timeout=180s
+152
View File
@@ -0,0 +1,152 @@
name: deploy
on:
workflow_dispatch:
concurrency:
group: deploy-main
cancel-in-progress: false
jobs:
redeploy:
runs-on: [self-hosted, linux, arch, homelab, prod]
steps:
- name: Redeploy workstation
shell: bash
env:
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
DEPLOY_PORT: ${{ secrets.DEPLOY_PORT }}
DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
DEPLOY_PATH: ${{ secrets.DEPLOY_PATH }}
DEPLOY_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
# Set APPLY_PRUNE=true to enable kubectl apply --prune. Requires every
# manifest to carry label app.kubernetes.io/managed-by=homelab-deploy,
# otherwise previously applied resources get deleted on the next run.
APPLY_PRUNE: ${{ vars.APPLY_PRUNE }}
run: |
set -euo pipefail
: "${DEPLOY_HOST:?missing DEPLOY_HOST}"
: "${DEPLOY_USER:?missing DEPLOY_USER}"
: "${DEPLOY_KEY:?missing DEPLOY_SSH_KEY}"
deploy_port="${DEPLOY_PORT:-22}"
deploy_path="${DEPLOY_PATH:-/srv/homelab}"
ssh_key="$RUNNER_TEMP/deploy_key"
mkdir -p "$RUNNER_TEMP"
printf '%s\n' "$DEPLOY_KEY" > "$ssh_key"
chmod 600 "$ssh_key"
ssh_opts=(
-i "$ssh_key"
-p "$deploy_port"
-o BatchMode=yes
-o StrictHostKeyChecking=accept-new
)
ssh "${ssh_opts[@]}" "${DEPLOY_USER}@${DEPLOY_HOST}" \
"DEPLOY_PATH=$(printf '%q' \"$deploy_path\") APPLY_PRUNE=$(printf '%q' \"${APPLY_PRUNE:-false}\") bash -se" <<'EOF'
set -euo pipefail
repo="${DEPLOY_PATH:-/srv/homelab}"
if [ ! -d "$repo/.git" ]; then
echo "Repository not found at $repo"
exit 1
fi
git -C "$repo" fetch origin main
git -C "$repo" reset --hard origin/main
# Runtime selection: a service is k8s-managed when $SERVICE/k8s/active
# exists. Otherwise it is compose-managed, and only k8s/routing/*
# manifests (external Services / EndpointSlices / ServersTransport /
# Ingresses that route to docker backends) are applied.
# migrate: touch SERVICE/k8s/active (+ move routing files up)
# rollback: rm SERVICE/k8s/active
collect_k8s() {
find "$1" -type f \( -name '*.yaml' -o -name '*.yml' \) \
! -path '*/routing/*' ! -path '*/overlays/*' \
! -name 'kustomization.y*ml' ! -name '*.example.y*ml' \
! -name '*values.y*ml' ! -name 'patch-*.y*ml' \
| sort
}
collect_k8s_inactive() {
find "$1" -type f \( -name '*.yaml' -o -name '*.yml' \) \
\( -name 'namespace.y*ml' -o -path '*/routing/*' \) \
! -path '*/overlays/*' ! -name '*.example.y*ml' \
| sort
}
mapfile -t compose_stacks < <(
find "$repo" -type f \( -name 'compose.yaml' -o -name 'compose.yml' \) | sort
)
mapfile -t k8s_manifests < <(
for kd in $(find "$repo" -type d -name k8s ! -path '*/.git/*' | sort); do
if [ -f "$kd/active" ]; then
collect_k8s "$kd"
else
collect_k8s_inactive "$kd"
fi
done
)
echo "== Validate compose stacks =="
for cf in "${compose_stacks[@]}"; do
dir=$(dirname "$cf")
if [ -f "$dir/k8s/active" ]; then
echo " skip (k8s-managed): $dir"
continue
fi
echo " config: $cf"
docker compose -f "$cf" config --quiet
done
echo "== Validate k8s manifests (kubectl dry-run) =="
for m in "${k8s_manifests[@]}"; do
echo " apply --dry-run=client $m"
kubectl apply --dry-run=client -f "$m" >/dev/null
done
echo "== Applying Kubernetes manifests =="
ns_files=()
other_files=()
for m in "${k8s_manifests[@]}"; do
case "$m" in
*/namespace.y?ml) ns_files+=("$m") ;;
*) other_files+=("$m") ;;
esac
done
prune_opts=()
if [ "${APPLY_PRUNE:-false}" = "true" ]; then
prune_opts=(--prune -l app.kubernetes.io/managed-by=homelab-deploy)
fi
if [ "${#ns_files[@]}" -gt 0 ]; then
echo " namespaces first: ${ns_files[*]}"
kubectl apply -f "${ns_files[@]}"
fi
if [ "${#other_files[@]}" -gt 0 ]; then
echo " resources: ${other_files[*]}"
kubectl apply "${prune_opts[@]}" -f "${other_files[@]}"
fi
echo "== Redeploying docker compose stacks =="
for cf in "${compose_stacks[@]}"; do
dir=$(dirname "$cf")
if [ -f "$dir/k8s/active" ]; then
echo " skip (k8s-managed): $dir"
continue
fi
echo " compose: $dir"
if grep -Eq '^\s+pull_policy:\s*build\b' "$cf"; then
docker compose -f "$cf" build
docker compose -f "$cf" push
fi
docker compose -f "$cf" up -d --pull always --remove-orphans
done
EOF
-39
View File
@@ -1,39 +0,0 @@
name: Deploy to Server
run-name: Deploying to ${{ runner.os}} server on ${{ gitea.ref }}
on:
push:
branches:
- main
- ci/gitea-actions
jobs:
deploy:
runs-on: prod
steps:
- name: Fetch and Diff Analysis
id: diff
run: |
cd ${{ secrets.PROD_DIR }}
git fetch origin main
CHANGES=$(git diff --name-only HEAD origin/main | cut -d/ -f1 | sort -u | tr '\n' ' ')
echo "dirs=$CHANGES" >> $GITHUB_OUTPUT
echo "Changed dirs: $CHANGES"
- name: Sync Server Files
run: |
cd ${{ secrets.PROD_DIR }}
git reset --hard origin/main
echo "Server files synced with origin/main"
- name: Deploy Services
run: |
cd ${{ secrets.PROD_DIR }}
for dir in ${{ steps.diff.outputs.dirs }}; do
if [ -d "$dir" ] && ([ -f "$dir/compose.yaml" ] || [ -f "$dir/docker-compose.yaml" ]); then
echo ">>> Deploying $dir"
cd "$dir"
DOCKER_BUILDKIT=1 BUILDKIT_PROGRESS=plain docker compose up -d --build --no-color
cd ..
else
echo ">>> Skipping $dir: no compose file found"
fi
done
+52
View File
@@ -0,0 +1,52 @@
name: renovate-ci
on:
pull_request:
push:
branches:
- main
workflow_dispatch:
jobs:
validate-renovate:
runs-on: [self-hosted, linux, arch, homelab]
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Validate Renovate Compose draft
shell: bash
run: |
set -euo pipefail
trap 'rm -f renovate/.env' EXIT
printf '%s\n' \
'RENOVATE_ENDPOINT=https://gitea.example/api/v1' \
'RENOVATE_TOKEN=test-token' \
'RENOVATE_REPOSITORIES=forust/homelab' \
> renovate/.env
docker compose -f renovate/renovate-compose.yaml config --quiet
- name: Validate Kubernetes manifests
shell: bash
run: |
set -euo pipefail
docker run --rm \
-v "$PWD:/work" \
-w /work \
ghcr.io/yannh/kubeconform:latest \
-strict \
-ignore-missing-schemas \
-summary \
renovate/k8s/namespace.yaml \
renovate/k8s/configmap.yaml \
renovate/k8s/cronjob.yaml
- name: Validate Renovate repository config
shell: bash
run: |
set -euo pipefail
docker run --rm \
-v "$PWD:/work" \
-w /work \
renovate/renovate:44.83.2 \
renovate-config-validator renovate.json
+370
View File
@@ -0,0 +1,370 @@
name: ci
on:
push:
branches:
- "**"
pull_request:
workflow_dispatch:
env:
REGISTRY: gcr.forust.xyz
jobs:
lint-prettier:
runs-on: [self-hosted, linux, arch, homelab]
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Check formatting with Prettier
shell: bash
run: |
mapfile -t prettier_files < <(
git ls-files \
| grep -E '\.(md|json|ya?ml|html|css)$' \
| grep -Ev '^(\.docs/|\.zed/|errorpages/html/|homepages/(forust_files|xdfnx_files)/)'
)
if [ "${#prettier_files[@]}" -eq 0 ]; then
echo "No Prettier-managed files found."
exit 0
fi
docker run --rm \
-v "$PWD:/work" \
-w /work \
node:22-alpine \
sh -lc 'npx --yes prettier@3 --check --ignore-unknown "$@"' sh "${prettier_files[@]}"
lint-ruff:
runs-on: [self-hosted, linux, arch, homelab]
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Lint Python with Ruff
shell: bash
run: |
docker run --rm \
-v "$PWD:/work" \
-w /work \
ghcr.io/astral-sh/ruff:latest \
check .
lint-yaml:
runs-on: [self-hosted, linux, arch, homelab]
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Lint YAML syntax
shell: bash
run: |
mapfile -t yaml_files < <(
git ls-files '*.yaml' '*.yml' \
':!node_modules/**' \
':!**/.venv/**'
)
if [ "${#yaml_files[@]}" -eq 0 ]; then
echo "No YAML files found."
exit 0
fi
docker run --rm \
-v "$PWD:/work" \
-w /work \
cytopia/yamllint:latest \
-c .yamllint "${yaml_files[@]}"
lint-dockerfiles:
runs-on: [self-hosted, linux, arch, homelab]
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Lint Dockerfiles
shell: bash
run: |
mapfile -t dockerfiles < <(
git ls-files ':(glob)**/Dockerfile' ':(glob)**/Dockerfile.*'
)
if [ "${#dockerfiles[@]}" -eq 0 ]; then
echo "No Dockerfiles found."
exit 0
fi
docker run --rm \
-v "$PWD:/work" \
-w /work \
--entrypoint hadolint \
hadolint/hadolint:latest-debian \
-c .hadolint.yaml "${dockerfiles[@]}"
validate:
runs-on: [self-hosted, linux, arch, homelab]
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Validate Kubernetes manifests
shell: bash
run: |
mapfile -t manifests < <(
git ls-files ':(glob)**/k8s/**/*.yaml' ':(glob)**/k8s/**/*.yml' \
| grep -Ev '(^|/)(kustomization\.ya?ml|.*\.example\.ya?ml|.*values\.ya?ml|patch-.*\.ya?ml)$'
)
if [ "${#manifests[@]}" -eq 0 ]; then
echo "No Kubernetes manifests found."
exit 0
fi
docker run --rm \
-v "$PWD:/work" \
-w /work \
ghcr.io/yannh/kubeconform:latest \
-strict \
-ignore-missing-schemas \
-summary \
"${manifests[@]}"
build:
needs: [lint-prettier, lint-ruff, lint-yaml, lint-dockerfiles, validate]
if: github.event_name != 'pull_request' && (github.ref_name == 'main' || github.ref_name == 'dev')
runs-on: [self-hosted, linux, arch, homelab]
outputs:
services: ${{ steps.services.outputs.services }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Detect changed docker-built services
id: services
shell: bash
run: |
base="${{ github.event.before }}"
if [ -z "$base" ] || [ "$base" = "0000000000000000000000000000000000000000" ]; then
base="$(git rev-list --max-parents=0 HEAD)"
fi
mapfile -t changed_files < <(git diff --name-only "$base" "${GITHUB_SHA}")
services=()
add_service() {
local name="$1"
local seen=0
for existing in "${services[@]}"; do
if [ "$existing" = "$name" ]; then
seen=1
break
fi
done
if [ "$seen" -eq 0 ]; then
services+=("$name")
fi
}
for file in "${changed_files[@]}"; do
case "$file" in
dtek_notif/*)
add_service dtek_notif
;;
errorpages/*)
add_service errorpages
;;
userbot/*)
add_service userbot
;;
homepages/*)
add_service homepages
;;
edu_master/phpsessid-bot/*|edu_master/webinar-checker/*|edu_master/compose.yaml)
add_service edu_master
;;
esac
done
if [ "${#services[@]}" -eq 0 ]; then
echo "No docker-built services changed."
echo "services=" >> "$GITHUB_OUTPUT"
exit 0
fi
printf '%s\n' "${services[@]}" | tee /tmp/services.txt
echo "services=$(paste -sd, /tmp/services.txt)" >> "$GITHUB_OUTPUT"
- name: Log in to registry
if: steps.services.outputs.services != ''
shell: bash
run: |
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login "${REGISTRY}" \
-u "${{ secrets.REGISTRY_USERNAME }}" \
--password-stdin
- name: Build and push changed images
if: steps.services.outputs.services != ''
shell: bash
run: |
IFS=, read -r -a services <<< "${{ steps.services.outputs.services }}"
for service in "${services[@]}"; do
case "$service" in
dtek_notif)
image="${REGISTRY}/forust/dtek-notif"
tags=("latest")
case "${GITHUB_REF_NAME}" in
main)
tags+=("main" "prod")
;;
dev)
tags+=("dev")
;;
esac
build_args=()
for tag in "${tags[@]}"; do
build_args+=(-t "${image}:${tag}")
done
docker build "${build_args[@]}" dtek_notif
for tag in "${tags[@]}"; do
docker push "${image}:${tag}"
done
;;
errorpages)
image="${REGISTRY}/forust/error-pages"
tags=("latest")
case "${GITHUB_REF_NAME}" in
main)
tags+=("main" "prod")
;;
dev)
tags+=("dev")
;;
esac
build_args=()
for tag in "${tags[@]}"; do
build_args+=(-t "${image}:${tag}")
done
docker build "${build_args[@]}" errorpages
for tag in "${tags[@]}"; do
docker push "${image}:${tag}"
done
;;
userbot)
tags=("latest")
case "${GITHUB_REF_NAME}" in
main)
tags+=("main" "prod")
;;
dev)
tags+=("dev")
;;
esac
for target in runtime panel; do
case "$target" in
runtime)
context="userbot"
image="${REGISTRY}/forust/userbot"
;;
panel)
context="userbot/panel"
image="${REGISTRY}/forust/userbot-panel"
;;
esac
build_args=()
for tag in "${tags[@]}"; do
build_args+=(-t "${image}:${tag}")
done
docker build "${build_args[@]}" "$context"
for tag in "${tags[@]}"; do
docker push "${image}:${tag}"
done
done
;;
homepages)
for service in forust xdfnx; do
case "$service" in
forust)
image="${REGISTRY}/forust/forust-homepage"
;;
xdfnx)
image="${REGISTRY}/forust/xdfnx-homepage"
;;
esac
tags=("latest")
case "${GITHUB_REF_NAME}" in
main)
tags+=("main" "prod")
;;
dev)
tags+=("dev")
;;
esac
build_args=()
for tag in "${tags[@]}"; do
build_args+=(-t "${image}:${tag}")
done
docker build "${build_args[@]}" -f "homepages/Dockerfile.${service}" homepages
for tag in "${tags[@]}"; do
docker push "${image}:${tag}"
done
done
;;
edu_master)
for service in session-keeper webinar-checker; do
case "$service" in
session-keeper)
context="edu_master/phpsessid-bot"
image="${REGISTRY}/forust/session-keeper"
;;
webinar-checker)
context="edu_master/webinar-checker"
image="${REGISTRY}/forust/webinar-checker"
;;
esac
tags=("latest")
case "${GITHUB_REF_NAME}" in
main)
tags+=("main" "prod")
;;
dev)
tags+=("dev")
;;
esac
build_args=()
for tag in "${tags[@]}"; do
build_args+=(-t "${image}:${tag}")
done
docker build "${build_args[@]}" "$context"
for tag in "${tags[@]}"; do
docker push "${image}:${tag}"
done
done
;;
esac
done
deploy-userbot-panel:
needs: build
if: github.ref_name == 'main' && contains(needs.build.outputs.services, 'userbot')
runs-on: [self-hosted, linux, arch, homelab, prod]
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Apply and roll out userbot panel
shell: bash
run: |
kubectl apply -f userbot/k8s/base/panel.yaml
kubectl get secret userbot-common-secrets -n default -o json \
| jq 'del(.metadata.annotations,.metadata.creationTimestamp,.metadata.resourceVersion,.metadata.uid,.metadata.managedFields) | .metadata.namespace = "userbot"' \
| kubectl apply -f -
# Keep legacy deployments (forust/anna) in sync with manifests; they have no replicas field, so apply leaves scaling to the user manager only.
kubectl apply -f userbot/k8s/base/userbots.yaml
kubectl rollout restart deployment/userbot-panel -n userbot
kubectl rollout status deployment/userbot-panel -n userbot --timeout=180s
+161
View File
@@ -0,0 +1,161 @@
name: deploy
on:
workflow_dispatch:
concurrency:
group: deploy-main
cancel-in-progress: false
jobs:
redeploy:
runs-on: [self-hosted, linux, arch, homelab, prod]
steps:
- name: Redeploy workstation
shell: bash
env:
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
DEPLOY_PORT: ${{ secrets.DEPLOY_PORT }}
DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
DEPLOY_PATH: ${{ secrets.DEPLOY_PATH }}
DEPLOY_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
# Set APPLY_PRUNE=true to enable kubectl apply --prune. Requires every
# manifest to carry label app.kubernetes.io/managed-by=homelab-deploy,
# otherwise previously applied resources get deleted on the next run.
APPLY_PRUNE: ${{ vars.APPLY_PRUNE }}
run: |
set -euo pipefail
: "${DEPLOY_HOST:?missing DEPLOY_HOST}"
: "${DEPLOY_USER:?missing DEPLOY_USER}"
: "${DEPLOY_KEY:?missing DEPLOY_SSH_KEY}"
deploy_port="${DEPLOY_PORT:-22}"
deploy_path="${DEPLOY_PATH:-/srv/homelab}"
ssh_key="$RUNNER_TEMP/deploy_key"
mkdir -p "$RUNNER_TEMP"
printf '%s\n' "$DEPLOY_KEY" > "$ssh_key"
chmod 600 "$ssh_key"
ssh_opts=(
-i "$ssh_key"
-p "$deploy_port"
-o BatchMode=yes
-o StrictHostKeyChecking=accept-new
)
ssh "${ssh_opts[@]}" "${DEPLOY_USER}@${DEPLOY_HOST}" \
"DEPLOY_PATH=$(printf '%q' \"$deploy_path\") APPLY_PRUNE=$(printf '%q' \"${APPLY_PRUNE:-false}\") bash -se" <<'EOF'
set -euo pipefail
repo="${DEPLOY_PATH:-/srv/homelab}"
if [ ! -d "$repo/.git" ]; then
echo "Repository not found at $repo"
exit 1
fi
git -C "$repo" fetch origin main
git -C "$repo" reset --hard origin/main
# Runtime selection: a service is k8s-managed when $SERVICE/k8s/active
# exists. Otherwise it is compose-managed, and only k8s/routing/*
# manifests (external Services / EndpointSlices / ServersTransport /
# Ingresses that route to docker backends) are applied.
# migrate: touch SERVICE/k8s/active (+ move routing files up)
# rollback: rm SERVICE/k8s/active
collect_k8s() {
find "$1" -type f \( -name '*.yaml' -o -name '*.yml' \) \
! -path '*/routing/*' ! -path '*/overlays/*' \
! -name 'kustomization.y*ml' ! -name '*.example.y*ml' \
! -name '*values.y*ml' ! -name 'patch-*.y*ml' \
| sort
}
collect_k8s_inactive() {
find "$1" -type f \( -name '*.yaml' -o -name '*.yml' \) \
\( -name 'namespace.y*ml' -o -path '*/routing/*' \) \
! -path '*/overlays/*' ! -name '*.example.y*ml' \
| sort
}
mapfile -t compose_stacks < <(
find "$repo" -type f \( -name 'compose.yaml' -o -name 'compose.yml' \) | sort
)
mapfile -t k8s_manifests < <(
for kd in $(find "$repo" -type d -name k8s ! -path '*/.git/*' | sort); do
if [ -f "$kd/active" ]; then
collect_k8s "$kd"
else
collect_k8s_inactive "$kd"
fi
done
)
echo "== Validate compose stacks =="
for cf in "${compose_stacks[@]}"; do
dir=$(dirname "$cf")
if [ -f "$dir/k8s/active" ]; then
echo " skip (k8s-managed): $dir"
continue
fi
echo " config: $cf"
docker compose -f "$cf" config --quiet
done
echo "== Validate k8s manifests (kubectl dry-run) =="
for m in "${k8s_manifests[@]}"; do
echo " apply --dry-run=client $m"
kubectl apply --dry-run=client -f "$m" >/dev/null
done
echo "== Applying Kubernetes manifests =="
ns_files=()
other_files=()
for m in "${k8s_manifests[@]}"; do
case "$m" in
*/namespace.y?ml) ns_files+=("$m") ;;
*) other_files+=("$m") ;;
esac
done
prune_opts=()
if [ "${APPLY_PRUNE:-false}" = "true" ]; then
prune_opts=(--prune -l app.kubernetes.io/managed-by=homelab-deploy)
fi
if [ "${#ns_files[@]}" -gt 0 ]; then
echo " namespaces first: ${ns_files[*]}"
kubectl apply -f "${ns_files[@]}"
fi
if [ -f "$repo/prometheus-stack/k8s/active" ]; then
echo "== Upgrading kube-prometheus-stack =="
helm upgrade --install prometheus-stack prometheus-community/kube-prometheus-stack \
--namespace prometheus \
--version 86.2.3 \
--values "$repo/prometheus-stack/k8s/grafana-values.yaml" \
--wait
fi
if [ "${#other_files[@]}" -gt 0 ]; then
echo " resources: ${other_files[*]}"
kubectl apply "${prune_opts[@]}" -f "${other_files[@]}"
fi
echo "== Redeploying docker compose stacks =="
for cf in "${compose_stacks[@]}"; do
dir=$(dirname "$cf")
if [ -f "$dir/k8s/active" ]; then
echo " skip (k8s-managed): $dir"
continue
fi
echo " compose: $dir"
if grep -Eq '^\s+pull_policy:\s*build\b' "$cf"; then
docker compose -f "$cf" build
docker compose -f "$cf" push
fi
docker compose -f "$cf" up -d --pull always --remove-orphans
done
EOF
-41
View File
@@ -1,41 +0,0 @@
## BINARY MODE, USE WITH THE GITHUB RUNNER BINARY INSTALLED ON THE SERVER
name: Deploy to Server
run-name: Deploying onto server on ${{ github.ref }}
on:
push:
branches:
- main
- ci/actions
jobs:
deploy:
runs-on: [prod, self-hosted]
steps:
- name: Fetch and Diff Analysis
id: diff
run: |
cd ${{ secrets.PROD_DIR }}
git fetch origin main
CHANGES=$(git diff --name-only HEAD origin/main | cut -d/ -f1 | sort -u | tr '\n' ' ')
echo "dirs=$CHANGES" >> $GITHUB_OUTPUT
echo "Changed dirs: $CHANGES"
- name: Sync Server Files
run: |
cd ${{ secrets.PROD_DIR }}
git reset --hard origin/main
echo "Server files synced with origin/main"
- name: Deploy Services
run: |
cd ${{ secrets.PROD_DIR }}
for dir in ${{ steps.diff.outputs.dirs }}; do
if [ -d "$dir" ] && ([ -f "$dir/compose.yaml" ] || [ -f "$dir/docker-compose.yaml" ]); then
echo ">>> Deploying $dir"
cd "$dir"
DOCKER_BUILDKIT=1 BUILDKIT_PROGRESS=plain docker compose up -d --build --no-color
cd ..
else
echo ">>> Skipping $dir: no compose file found"
fi
done
+5 -5
View File
@@ -32,7 +32,7 @@ streaming/data/*
streaming/qbittorrent/*
streaming/prowlarr/*
# Homepage
# Homepage
homepages/forust_files/.well-known/*
# Traefik files
@@ -40,7 +40,8 @@ traefik/letsencrypt/acme.json
traefik/dynamic/fileservers.yml
traefik/dynamic/*.local.y*ml.*
traefik/dynamic/*.external.y*ml
traefik/k8s/fileservers.y*ml
traefik/k8s/aliasHeadersStrategy.md
traefik/logs/*
@@ -52,7 +53,7 @@ certs/
# Monitoring
monitoring/prometheus.yml
# Python
# Python
.python-version
venv/
pyc
@@ -93,7 +94,7 @@ replacements.txt
edu_master/temp/
temp/*
# Environment
# Environment
.env
.env.anna
.env.forust
@@ -106,4 +107,3 @@ temp/*
traefik/k8s/local-tls.yaml
converters/k8s/config.yaml
convertx/k8s/config.yaml
traefik/k8s/crowdsec-middleware.yaml
+13 -12
View File
@@ -9,31 +9,32 @@
"hard_tabs": false,
"format_on_save": "on",
"formatter": {
"language_server": { "name": "yaml-language-server" }
}
"language_server": { "name": "yaml-language-server" },
},
},
"Python": {
"tab_size": 4,
"format_on_save": "on",
"language_servers": ["pyright", "ruff"],
"formatter": {
"language_server": { "name": "ruff" }
}
}
"language_server": { "name": "ruff" },
},
},
},
"lsp": {
"yaml-language-server": {
"settings": {
"yaml": {
"schemas": {
"kubernetes": ["**/k8s/*.yaml", "**/k8s/*.yml"]
"kubernetes": ["**/k8s/*.yaml", "**/k8s/*.yml"],
},
"validate": true,
"completion": true,
"format": {
"enable": true
}
}
}
}
}
"enable": true,
},
},
},
},
},
}
+1 -1
View File
@@ -1,6 +1,6 @@
services:
adguard:
image: adguard/adguardhome:latest
image: adguard/adguardhome:v0.107.79
container_name: adguardhome
restart: unless-stopped
ports:
View File
Whitespace-only changes.
+1 -1
View File
@@ -65,7 +65,7 @@ spec:
spec:
containers:
- name: adguard
image: adguard/adguardhome:latest
image: adguard/adguardhome:v0.107.79
resources:
limits:
memory: "1.5Gi"
+8 -16
View File
@@ -10,7 +10,13 @@ spec:
- match: Host(`adguard.forust.xyz`) || Host(`dns.forust.xyz`)
kind: Rule
middlewares:
- name: "crowdsec-crowdsec-bouncer@kubernetescrd"
- name: crowdsec-bouncer
namespace: crowdsec
services:
- name: adguard-service
port: 3000
- match: (Host(`adguard.forust.xyz`) || Host(`dns.forust.xyz`)) && PathPrefix(`/dns-query`)
kind: Rule
services:
- name: adguard-service
port: 3000
@@ -31,22 +37,8 @@ spec:
services:
- name: adguard-service
port: 3000
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: adguard-doh
namespace: adguard
spec:
entryPoints:
- websecure
routes:
- match: (Host(`adguard.forust.xyz`) || Host(`dns.forust.xyz`)) && PathPrefix(`/dns-query`)
- match: (Host(`adguard.workstation.internal`) || Host(`dns.workstation.internal`) || Host(`adguard.gigaforust.internal`) || Host(`dns.gigaforust.internal`)) && PathPrefix(`/dns-query`)
kind: Rule
middlewares:
- name: "crowdsec-crowdsec-bouncer@kubernetescrd"
services:
- name: adguard-service
port: 3000
tls:
certResolver: letsencrypt
+1 -1
View File
@@ -1,6 +1,6 @@
services:
postgresql:
image: docker.io/library/postgres:15-alpine
image: docker.io/library/postgres:15.19-alpine
restart: unless-stopped
env_file:
- .env
View File
Whitespace-only changes.
+1 -1
View File
@@ -6,6 +6,6 @@ metadata:
data:
AUTHENTIK_IMAGE: ghcr.io/goauthentik/server
AUTHENTIK_TAG: "2025.10.2"
AUTHENTIK_POSTGRESQL__HOST: authentik-postgres-service
AUTHENTIK_POSTGRESQL__HOST: postgres.database.svc.cluster.local
AUTHENTIK_POSTGRESQL__NAME: authentik
AUTHENTIK_ERROR_REPORTING__ENABLED: "true"
+2 -1
View File
@@ -10,7 +10,8 @@ spec:
- match: Host(`auth.forust.xyz`)
kind: Rule
middlewares:
- name: "crowdsec-crowdsec-bouncer@kubernetescrd"
- name: crowdsec-bouncer
namespace: crowdsec
services:
- name: authentik-server-service
port: 9000
-66
View File
@@ -1,66 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: authentik-postgres-service
namespace: authentik
spec:
clusterIP: None
selector:
app: authentik-postgres
ports:
- port: 5432
targetPort: 5432
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: authentik-postgres-statefulset
namespace: authentik
spec:
selector:
matchLabels:
app: authentik-postgres
serviceName: authentik-postgres-service
replicas: 1
template:
metadata:
labels:
app: authentik-postgres
spec:
containers:
- name: postgres
image: docker.io/library/postgres:15-alpine
env:
- name: POSTGRES_DB
value: authentik
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: authentik-secrets
key: AUTHENTIK_POSTGRESQL__USER
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: authentik-secrets
key: AUTHENTIK_POSTGRESQL__PASSWORD
ports:
- containerPort: 5432
name: postgres
volumeMounts:
- name: postgres-data
mountPath: /var/lib/postgresql/data
resources:
requests:
memory: "256Mi"
cpu: "200m"
limits:
memory: "1Gi"
cpu: "500m"
volumeClaimTemplates:
- metadata:
name: postgres-data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 5Gi
+1 -1
View File
@@ -1,6 +1,6 @@
services:
cloudflare-ddns:
image: timothyjmiller/cloudflare-ddns:latest
image: timothyjmiller/cloudflare-ddns:2.2.0
container_name: cloudflare-ddns
restart: unless-stopped
security_opt:
+1 -1
View File
@@ -7,7 +7,7 @@
"api_key": {
"api_key": "api_key_here",
"account_email": "your_email_here"
}
},
"zone_id": "your_zone-id",
"subdomains": [
{ "name": "", "proxied": true },
View File
Whitespace-only changes.
+1 -1
View File
@@ -18,7 +18,7 @@ spec:
dnsPolicy: ClusterFirstWithHostNet
containers:
- name: cloudflare-ddns
image: timothyjmiller/cloudflare-ddns:latest
image: timothyjmiller/cloudflare-ddns:2.2.0
imagePullPolicy: Always
resources:
requests:
+1 -1
View File
@@ -1,6 +1,6 @@
services:
checkmk:
image: "checkmk/check-mk-raw:2.4.0-latest"
image: "checkmk/check-mk-raw:2.4.0-2026.09.14"
container_name: "checkmk"
restart: unless-stopped
# ports:
View File
Whitespace-only changes.
+10 -3
View File
@@ -7,8 +7,12 @@ spec:
selector:
app: checkmk
ports:
- port: 5000
- name: web
port: 5000
targetPort: 5000
- name: agent-receiver
port: 8000
targetPort: 8000
---
apiVersion: apps/v1
kind: Deployment
@@ -27,14 +31,17 @@ spec:
spec:
containers:
- name: checkmk
image: checkmk/check-mk-raw:2.4.0-latest
image: checkmk/check-mk-raw:2.4.0-2026.09.14
envFrom:
- secretRef:
name: checkmk-secrets
- configMapRef:
name: checkmk-config
ports:
- containerPort: 5000
- name: web
containerPort: 5000
- name: agent-receiver
containerPort: 8000
volumeMounts:
- name: sites
mountPath: /omd/sites
+18 -1
View File
@@ -10,7 +10,8 @@ spec:
- match: Host(`cmk.forust.xyz`)
kind: Rule
middlewares:
- name: crowdsec-crowdsec-bouncer@kubernetescrd
- name: crowdsec-bouncer
namespace: crowdsec
services:
- name: checkmk-service
port: 5000
@@ -18,6 +19,22 @@ spec:
certResolver: letsencrypt
---
apiVersion: traefik.io/v1alpha1
kind: IngressRouteTCP
metadata:
name: checkmk-agent-receiver
namespace: checkmk
spec:
entryPoints:
- checkmk-agent
routes:
- match: HostSNI(`*`)
services:
- name: checkmk-service
port: 8000
tls:
passthrough: true
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: checkmk-local
+2 -2
View File
@@ -1,7 +1,7 @@
services:
convertx:
container_name: convertx
image: ghcr.io/c4illin/convertx:latest
image: ghcr.io/c4illin/convertx:v0.18.0
restart: unless-stopped
ports:
- "9992:3000"
@@ -42,7 +42,7 @@ services:
bentopdf:
container_name: bentopdf
image: bentopdf/bentopdf:latest
image: bentopdf/bentopdf@sha256:4eb4ec8f5030faf87c29a73d3d5a2781f28a597cf440c3ab111eb96aee550871
restart: unless-stopped
labels:
- "traefik.enable=true"
View File
Whitespace-only changes.
+1 -1
View File
@@ -26,7 +26,7 @@ spec:
app: bentopdf
spec:
containers:
- image: bentopdf/bentopdf:latest
- image: bentopdf/bentopdf@sha256:4eb4ec8f5030faf87c29a73d3d5a2781f28a597cf440c3ab111eb96aee550871
imagePullPolicy: Always
name: bentopdf
ports:
+1 -1
View File
@@ -26,7 +26,7 @@ spec:
app: convertx
spec:
containers:
- image: ghcr.io/c4illin/convertx:latest
- image: ghcr.io/c4illin/convertx:v0.18.0
name: convertx
envFrom:
- configMapRef:
+28 -28
View File
@@ -2,23 +2,9 @@ container_runtime: containerd
agent:
env:
- name: COLLECTIONS
value: "crowdsecurity/traefik crowdsecurity/base-http-scenarios crowdsecurity/sshd"
extraVolumes:
- name: journal-dir
hostPath:
path: /var/log/journal
type: DirectoryOrCreate
- name: run-journal-dir
hostPath:
path: /run/log/journal
type: DirectoryOrCreate
extraVolumeMounts:
- name: journal-dir
mountPath: /var/log/journal
readOnly: true
- name: run-journal-dir
mountPath: /run/log/journal
readOnly: true
value: "crowdsecurity/traefik crowdsecurity/base-http-scenarios"
- name: DISABLE_COLLECTIONS
value: "crowdsecurity/linux crowdsecurity/sshd"
acquisition:
- namespace: traefik
@@ -26,13 +12,6 @@ agent:
program: traefik
poll_without_inotify: true
acquisitionCustom: |
- source: journalctl
journalctl_filter:
- _SYSTEMD_UNIT=sshd.service
labels:
type: syslog
resources:
requests:
cpu: 50m
@@ -44,14 +23,35 @@ agent:
lapi:
env:
- name: COLLECTIONS
value: "crowdsecurity/traefik crowdsecurity/base-http-scenarios crowdsecurity/sshd"
value: "crowdsecurity/traefik crowdsecurity/base-http-scenarios"
- name: DISABLE_COLLECTIONS
value: "crowdsecurity/linux crowdsecurity/sshd"
service:
type: NodePort
nodePort: 30011
type: ClusterIP
persistentVolume:
data:
enabled: true
storageClassName: local-path-retain
size: 1Gi
config:
enabled: true
storageClassName: local-path-retain
size: 100Mi
storeLAPICscliCredentialsInSecret: true
resources:
requests:
cpu: 50m
memory: 150Mi
limits:
cpu: 200m
cpu: 400m
memory: 500Mi
metrics:
enabled: true
serviceMonitor:
additionalLabels:
release: prometheus-stack
enabled: true
interval: 30s
scrapeTimeout: 10s
namespace: prometheus
+6
View File
@@ -0,0 +1,6 @@
apiVersion: v1
kind: Namespace
metadata:
name: crowdsec
labels:
app.kubernetes.io/part-of: crowdsec
+27
View File
@@ -0,0 +1,27 @@
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: crowdsec-lapi
namespace: crowdsec
spec:
podSelector:
matchLabels:
k8s-app: crowdsec
type: lapi
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: traefik
podSelector:
matchLabels:
app.kubernetes.io/name: traefik
- podSelector:
matchLabels:
k8s-app: crowdsec
type: agent
ports:
- protocol: TCP
port: 8080
+1 -1
View File
@@ -1,6 +1,6 @@
services:
dockmon:
image: darthnorse/dockmon:latest
image: darthnorse/dockmon:2.4.5
container_name: dockmon
restart: unless-stopped
# ports:
View File
Whitespace-only changes.
+1 -1
View File
@@ -29,7 +29,7 @@ spec:
spec:
containers:
- name: dockmon
image: darthnorse/dockmon:latest
image: darthnorse/dockmon:2.4.5
ports:
- containerPort: 443
volumeMounts:
+2 -1
View File
@@ -18,7 +18,8 @@ spec:
- match: Host(`dockmon.forust.xyz`)
kind: Rule
middlewares:
- name: crowdsec-crowdsec-bouncer@kubernetescrd
- name: crowdsec-bouncer
namespace: crowdsec
- name: security-headers@file
services:
- name: dockmon-service
+1 -1
View File
@@ -1,7 +1,7 @@
services:
downtify:
container_name: downtify
image: ghcr.io/henriquesebastiao/downtify:latest
image: ghcr.io/henriquesebastiao/downtify:2.12.0
restart: unless-stopped
# ports:
# - '7077:8000'
+1 -1
View File
@@ -27,7 +27,7 @@ spec:
spec:
containers:
- name: downtify
image: ghcr.io/henriquesebastiao/downtify:latest
image: ghcr.io/henriquesebastiao/downtify:2.12.0
ports:
- containerPort: 8000
volumeMounts:
+2 -1
View File
@@ -10,7 +10,8 @@ spec:
- match: Host(`downtify.forust.xyz`)
kind: Rule
middlewares:
- name: crowdsec-crowdsec-bouncer@kubernetescrd
- name: crowdsec-bouncer
namespace: crowdsec
- name: security-chain@file
services:
- name: downtify-service
+388 -418
View File
File diff suppressed because it is too large. Load diff
+1
View File
@@ -9,5 +9,6 @@ WEBINAR_CHECK_INTERVAL=60
REDIS_HOST=redis
REDIS_PORT=6379
PLAYWRIGHT_WS=ws://playwright-service:3000/ws
TZ=Europe/Kyiv
WEBINAR_TELEGRAM_TOKEN=your_telegram_bot_token_here
WEBINAR_ADMIN_ID=123456789
+2 -2
View File
@@ -1,6 +1,6 @@
services:
redis:
image: redis:alpine
image: redis:8.0.3-alpine
restart: unless-stopped
volumes:
- redis-data:/data
@@ -11,7 +11,7 @@ services:
retries: 5
playwright-service:
image: mcr.microsoft.com/playwright:v1.56.0-jammy
image: mcr.microsoft.com/playwright:v1.56.1-jammy
restart: unless-stopped
command: npx -y playwright@1.56.0 run-server --port 3000 --path /ws
View File
Whitespace-only changes.
+4
View File
@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: edu-master
+57
View File
@@ -0,0 +1,57 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: playwright-service
namespace: edu-master
labels:
app: edu-master-playwright
spec:
replicas: 1
selector:
matchLabels:
app: edu-master-playwright
template:
metadata:
labels:
app: edu-master-playwright
spec:
containers:
- name: playwright
image: mcr.microsoft.com/playwright:v1.56.1-jammy
imagePullPolicy: IfNotPresent
command:
- npx
- -y
- playwright@1.56.0
- run-server
- --port
- "3000"
- --path
- /ws
ports:
- containerPort: 3000
readinessProbe:
tcpSocket:
port: 3000
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 3
livenessProbe:
tcpSocket:
port: 3000
initialDelaySeconds: 15
periodSeconds: 20
timeoutSeconds: 3
---
apiVersion: v1
kind: Service
metadata:
name: playwright-service
namespace: edu-master
spec:
selector:
app: edu-master-playwright
ports:
- name: ws
port: 3000
targetPort: 3000
+75
View File
@@ -0,0 +1,75 @@
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: redis
namespace: edu-master
labels:
app: edu-master-redis
spec:
serviceName: redis
replicas: 1
selector:
matchLabels:
app: edu-master-redis
template:
metadata:
labels:
app: edu-master-redis
spec:
containers:
- name: redis
image: redis:8.0.3-alpine
imagePullPolicy: IfNotPresent
ports:
- containerPort: 6379
volumeMounts:
- name: redis-data
mountPath: /data
resources:
requests:
cpu: 25m
memory: 64Mi
limits:
cpu: 250m
memory: 256Mi
readinessProbe:
exec:
command: ["redis-cli", "ping"]
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 3
livenessProbe:
exec:
command: ["redis-cli", "ping"]
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 3
volumes:
- name: redis-data
persistentVolumeClaim:
claimName: redis-data-pvc
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: redis-data-pvc
namespace: edu-master
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
---
apiVersion: v1
kind: Service
metadata:
name: redis
namespace: edu-master
spec:
selector:
app: edu-master-redis
ports:
- name: redis
port: 6379
targetPort: 6379
@@ -0,0 +1,50 @@
# One-time Job to migrate redis state from docker compose to k8s (maintenance window).
# The .example file is not applied by the deploy pipeline (mask *.example.yaml).
#
# Runbook:
# 1. docker compose -f <repo>/edu_master/compose.yaml stop # SIGTERM -> redis will flush dump.rdb
# 2. docker run --rm -v edu_master_redis-data:/data \
# -v /tmp/edu-master-backup:/backup \
# redis:alpine sh -c "cp /data/dump.rdb /backup/ && ls -la /backup"
# 3. kubectl apply -f edu_master/k8s/namespace.yaml
# 4. kubectl apply -f <only the PVC from redis.yaml> # seed must come BEFORE redis pod starts
# 5. kubectl apply -f edu_master/k8s/restore-seed-job.yaml.example
# kubectl wait --for=condition=complete job/redis-restore-seed -n edu-master --timeout=120s
# 6. kubectl delete job redis-restore-seed -n edu-master
# 7. kubectl apply -f edu_master/k8s/ -R # apply remaining manifests
apiVersion: batch/v1
kind: Job
metadata:
name: redis-restore-seed
namespace: edu-master
spec:
backoffLimit: 2
ttlSecondsAfterFinished: 3600
template:
spec:
restartPolicy: Never
containers:
- name: seed
image: redis:alpine
command:
- /bin/sh
- -ec
- |
ls -la /backup
cp /backup/dump.rdb /data/dump.rdb
chmod 644 /data/dump.rdb
ls -la /data
volumeMounts:
- name: redis-data
mountPath: /data
- name: backup
mountPath: /backup
readOnly: true
volumes:
- name: redis-data
persistentVolumeClaim:
claimName: redis-data-pvc
- name: backup
hostPath:
path: /tmp/edu-master-backup
type: DirectoryOrCreate
+27
View File
@@ -0,0 +1,27 @@
apiVersion: v1
kind: Secret
metadata:
name: edu-master-secrets
namespace: edu-master
type: Opaque
stringData:
# Session keeper credentials
KEEPER_LOGIN: ""
KEEPER_PASSWORD: ""
KEEPER_INTERVAL: "10"
# EDU links
EDU_URL_BASE: "https://edu.edu.vn.ua"
EDU_URL_LOGIN: "/user/login"
EDU_URL_COURSES: "/course/userlist"
EDU_URL_WEBINAR: "/webinar/useractive"
# Playwright
USER_AGENT: ""
PLAYWRIGHT_WS: "ws://playwright-service:3000/ws"
# Webinar-checker
WEBINAR_TELEGRAM_TOKEN: ""
WEBINAR_ADMIN_ID: ""
WEBINAR_CHECK_INTERVAL: "60"
# Database
REDIS_HOST: "redis"
REDIS_PORT: "6379"
TZ: "Europe/Kyiv"
+52
View File
@@ -0,0 +1,52 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: session-keeper
namespace: edu-master
labels:
app: edu-master-session-keeper
spec:
replicas: 1
selector:
matchLabels:
app: edu-master-session-keeper
template:
metadata:
labels:
app: edu-master-session-keeper
spec:
initContainers:
- name: wait-redis
image: redis:8.0.3-alpine
command:
- /bin/sh
- -ec
- |
i=0
until redis-cli -h redis ping | grep -q PONG; do
i=$((i+1))
[ "$i" -ge 300 ] && echo "TIMEOUT: redis not ready" && exit 1
sleep 2
done
echo "redis is ready"
containers:
- name: session-keeper
image: gcr.forust.xyz/forust/session-keeper:latest
imagePullPolicy: Always
envFrom:
- secretRef:
name: edu-master-secrets
resources:
requests:
cpu: 25m
memory: 96Mi
limits:
cpu: 250m
memory: 256Mi
readinessProbe:
exec:
command: ["/bin/sh", "-ec", "redis-cli -h redis EXISTS EDU_PHPSESSID | grep -q 1"]
initialDelaySeconds: 15
periodSeconds: 30
timeoutSeconds: 5
failureThreshold: 10
+62
View File
@@ -0,0 +1,62 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: webinar-checker
namespace: edu-master
labels:
app: edu-master-webinar-checker
spec:
replicas: 1
selector:
matchLabels:
app: edu-master-webinar-checker
template:
metadata:
labels:
app: edu-master-webinar-checker
spec:
# Enforces dependency order like compose depends_on:
# redis healthy -> session-keeper healthy (EXISTS EDU_PHPSESSID) -> playwright started
initContainers:
- name: wait-deps
image: redis:8.0.3-alpine
command:
- /bin/sh
- -ec
- |
i=0
until redis-cli -h redis ping | grep -q PONG; do
i=$((i+1))
[ "$i" -ge 300 ] && echo "TIMEOUT: redis not ready" && exit 1
sleep 2
done
echo "redis ok"
until [ "$(redis-cli -h redis EXISTS EDU_PHPSESSID)" = "1" ]; do
i=$((i+1))
[ "$i" -ge 300 ] && echo "TIMEOUT: no PHPSESSID (session-keeper down?)" && exit 1
sleep 2
done
echo "PHPSESSID ok"
until nc -z playwright-service 3000; do
i=$((i+1))
[ "$i" -ge 300 ] && echo "TIMEOUT: playwright-service not reachable" && exit 1
sleep 2
done
echo "playwright ok"
containers:
- name: webinar-checker
image: gcr.forust.xyz/forust/webinar-checker:latest
imagePullPolicy: Always
envFrom:
- secretRef:
name: edu-master-secrets
env:
- name: TZ
value: "Europe/Kyiv"
resources:
requests:
cpu: "50m"
memory: "128Mi"
limits:
cpu: "600m"
memory: "512Mi"
+2 -2
View File
@@ -3,10 +3,10 @@ FROM python:3.11-slim
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y redis-tools && rm -rf /var/lib/apt/lists/*
RUN apt-get update && apt-get install -y --no-install-recommends redis-tools && rm -rf /var/lib/apt/lists/*
# Install dependencies
RUN pip install requests redis
RUN pip install --no-cache-dir requests==2.32.3 redis==5.2.1
# Copy application code
COPY . .
+46 -43
View File
@@ -1,17 +1,16 @@
import logging
import os
import time
import requests
import logging
import redis
from datetime import datetime
import redis
import requests
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
# Load configuration (adapted to .env keys)
def _env(key, default=None):
v = os.getenv(key, default)
@@ -19,21 +18,26 @@ def _env(key, default=None):
return v[1:-1]
return v
LOGIN = _env('KEEPER_LOGIN')
PASSWORD = _env('KEEPER_PASSWORD')
EDU_BASE = _env('EDU_URL_BASE', 'https://edu.edu.vn.ua')
EDU_LOGIN_PATH = _env('EDU_URL_LOGIN', '/user/login')
EDU_COURSES_PATH = _env('EDU_URL_COURSES', '/course/userlist')
URL_LOGIN = f"{EDU_BASE.rstrip('/')}/{EDU_LOGIN_PATH.lstrip('/')}"
URL_VERIFY = f"{EDU_BASE.rstrip('/')}/{EDU_COURSES_PATH.lstrip('/')}"
URL_LOGIN = f'{EDU_BASE.rstrip("/")}/{EDU_LOGIN_PATH.lstrip("/")}'
URL_VERIFY = f'{EDU_BASE.rstrip("/")}/{EDU_COURSES_PATH.lstrip("/")}'
INTERVAL = int(_env('KEEPER_INTERVAL', 10))
USER_AGENT = _env('USER_AGENT', 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36')
USER_AGENT = _env(
'USER_AGENT',
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36',
)
REDIS_HOST = _env('REDIS_HOST', 'redis')
REDIS_PORT = int(_env('REDIS_PORT', 6379))
SUCCESS_FILE = '/tmp/last_success'
SUCCESS_FILE = '/tmp/last_success' # noqa: S108
def touch_success_file():
"""Updates the timestamp of the success file for healthchecks."""
@@ -41,22 +45,23 @@ def touch_success_file():
with open(SUCCESS_FILE, 'w') as f:
f.write(str(datetime.now().timestamp()))
except Exception as e:
logger.error(f"Failed to touch success file: {e}")
logger.error(f'Failed to touch success file: {e}')
def main():
logger.info("Starting Session Keeper Bot")
logger.info('Starting Session Keeper Bot')
# Connect to Redis
try:
redis_client = redis.Redis(host=REDIS_HOST, port=REDIS_PORT, decode_responses=True)
redis_client.ping()
logger.info(f"Connected to Redis at {REDIS_HOST}:{REDIS_PORT}")
logger.info(f'Connected to Redis at {REDIS_HOST}:{REDIS_PORT}')
except Exception as e:
logger.error(f"Failed to connect to Redis: {e}")
logger.error(f'Failed to connect to Redis: {e}')
return
session = requests.Session()
# Set headers
headers = {
'User-Agent': USER_AGENT,
@@ -72,58 +77,56 @@ def main():
'Sec-Ch-Ua-Mobile': '?0',
'Sec-Ch-Ua-Platform': '"Linux"',
'Accept-Encoding': 'gzip, deflate, br',
'Priority': 'u=0, i'
'Priority': 'u=0, i',
}
session.headers.update(headers)
while True:
try:
logger.info("Attempting login...")
logger.info('Attempting login...')
# Login payload
payload = {
'login': LOGIN,
'password': PASSWORD
}
payload = {'login': LOGIN, 'password': PASSWORD}
# Perform Login
# Note: The user request shows a POST to /user/login with form data
# We need to make sure we handle the PHPSESSID correctly.
# If we already have a PHPSESSID, requests will send it.
login_response = session.post(URL_LOGIN, data=payload, allow_redirects=True)
logger.info(f"Login Response Status: {login_response.status_code}")
logger.info(f"Cookies after login: {session.cookies.get_dict()}")
logger.info(f'Login Response Status: {login_response.status_code}')
logger.info(f'Cookies after login: {session.cookies.get_dict()}')
# Verify Session
logger.info("Verifying session...")
logger.info('Verifying session...')
verify_response = session.get(URL_VERIFY, allow_redirects=False)
logger.info(f"Verify Response Status: {verify_response.status_code}")
logger.info(f'Verify Response Status: {verify_response.status_code}')
if verify_response.status_code == 200:
logger.info("Session verification SUCCESS (200 OK).")
logger.info('Session verification SUCCESS (200 OK).')
touch_success_file()
# Save PHPSESSID to Redis
phpsessid = session.cookies.get('PHPSESSID')
if phpsessid:
try:
redis_client.set('EDU_PHPSESSID', phpsessid)
logger.info(f"Saved PHPSESSID to Redis: {phpsessid}")
logger.info(f'Saved PHPSESSID to Redis: {phpsessid}')
except Exception as e:
logger.error(f"Failed to save PHPSESSID to Redis: {e}")
logger.error(f'Failed to save PHPSESSID to Redis: {e}')
elif verify_response.status_code == 302:
logger.warning("Session verification FAILED (302 Redirect). Session might be invalid.")
logger.warning('Session verification FAILED (302 Redirect). Session might be invalid.')
else:
logger.warning(f"Session verification returned unexpected status: {verify_response.status_code}")
logger.warning(f'Session verification returned unexpected status: {verify_response.status_code}')
except Exception as e:
logger.error(f"An error occurred: {e}")
logger.error(f'An error occurred: {e}')
logger.info(f"Sleeping for {INTERVAL} minutes...")
logger.info(f'Sleeping for {INTERVAL} minutes...')
time.sleep(INTERVAL * 60)
if __name__ == "__main__":
if __name__ == '__main__':
main()
+1 -1
View File
@@ -3,7 +3,7 @@ FROM python:3.11-slim
WORKDIR /app
# Install dependencies
RUN pip install --upgrade pip && pip install playwright==1.56.0 redis requests "python-telegram-bot[job-queue]"
RUN pip install --no-cache-dir pip==25.0.1 && pip install --no-cache-dir playwright==1.56.0 redis==5.2.1 requests==2.32.3 "python-telegram-bot[job-queue]==21.10"
COPY checker.py .
File diff suppressed because it is too large. Load diff
+2 -2
View File
@@ -1,6 +1,6 @@
services:
server:
image: docker.gitea.com/gitea:1.26
image: docker.gitea.com/gitea:1.27.3
container_name: gitea
restart: always
environment:
@@ -61,7 +61,7 @@ services:
depends_on:
- db
db:
image: docker.io/library/postgres:14
image: docker.io/library/postgres:14.24-alpine
restart: always
environment:
- POSTGRES_USER=gitea
View File
Whitespace-only changes.
+3 -1
View File
@@ -10,11 +10,13 @@ data:
GITEA__server__SSH_PORT: "2221"
GITEA__database__DB_TYPE: "postgres"
GITEA__database__HOST: "gitea-postgres-service:5432"
GITEA__database__HOST: "postgres.database.svc.cluster.local:5432"
GITEA__database__NAME: "gitea"
GITEA__security__REVERSE_PROXY_LIMIT: "1"
GITEA__security__REVERSE_PROXY_TRUSTED_PROXIES: "*"
GITEA__mailer__ENABLED: "false"
GITEA__log__logger.access.MODE: "console, file"
USER_UID: "1000"
USER_GID: "1000"
+1 -1
View File
@@ -31,7 +31,7 @@ spec:
spec:
containers:
- name: gitea
image: docker.gitea.com/gitea:1.26
image: docker.gitea.com/gitea:1.27.3
envFrom:
- configMapRef:
name: gitea-config
+11 -14
View File
@@ -10,7 +10,16 @@ spec:
- match: Host(`gitea.forust.xyz`)
kind: Rule
middlewares:
- name: "crowdsec-crowdsec-bouncer@kubernetescrd"
- name: crowdsec-bouncer
namespace: crowdsec
services:
- name: gitea-service
port: 3000
- match: Host(`gcr.forust.xyz`) && PathPrefix(`/v2`)
kind: Rule
middlewares:
- name: crowdsec-bouncer
namespace: crowdsec
services:
- name: gitea-service
port: 3000
@@ -31,23 +40,11 @@ spec:
services:
- name: gitea-service
port: 3000
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: gitea-registry
namespace: gitea
spec:
entryPoints:
- websecure
routes:
- match: Host(`gcr.forust.xyz`) && PathPrefix(`/v2`)
- match: (Host(`gcr.workstation.internal`) || Host(`gcr.gigaforust.internal`)) && PathPrefix(`/v2`)
kind: Rule
services:
- name: gitea-service
port: 3000
tls:
certResolver: letsencrypt
---
apiVersion: traefik.io/v1alpha1
kind: IngressRouteTCP
-62
View File
@@ -1,62 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: gitea-postgres-service
namespace: gitea
spec:
clusterIP: None
selector:
app: gitea-postgres
ports:
- port: 5432
targetPort: 5432
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: gitea-postgres-statefulset
namespace: gitea
spec:
selector:
matchLabels:
app: gitea-postgres
serviceName: gitea-postgres-service
replicas: 1
template:
metadata:
labels:
app: gitea-postgres
spec:
containers:
- name: gitea-postgres
image: postgres:14
env:
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: gitea-secrets
key: GITEA__database__USER
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: gitea-secrets
key: GITEA__database__PASSWD
- name: POSTGRES_DB
valueFrom:
secretKeyRef:
name: gitea-secrets
key: GITEA__database__USER
ports:
- containerPort: 5432
name: postgres
volumeMounts:
- name: postgres-data
mountPath: /var/lib/postgresql/data
volumeClaimTemplates:
- metadata:
name: postgres-data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 1Gi
+1
View File
@@ -7,3 +7,4 @@ type: Opaque
stringData:
GITEA__database__USER: "gitea"
GITEA__database__PASSWD: "gitea"
GITEA__database__NAME: "gitea"
+1 -1
View File
@@ -1,7 +1,7 @@
services:
glance:
container_name: glance
image: glanceapp/glance
image: glanceapp/glance:v0.8.6
restart: unless-stopped
volumes:
- ./config:/app/config:ro
View File
Whitespace-only changes.
+5 -5
View File
@@ -27,7 +27,7 @@ spec:
spec:
containers:
- name: glance
image: glanceapp/glance
image: glanceapp/glance:v0.8.6
envFrom:
- secretRef:
name: glance-secrets
@@ -56,11 +56,11 @@ spec:
readOnly: true
resources:
requests:
memory: "30Mi"
cpu: "20m"
limits:
memory: "100Mi"
cpu: "50m"
memory: "64Mi"
limits:
cpu: "200m"
memory: "256Mi"
volumes:
- name: glance-config
configMap:
+37 -3
View File
@@ -1,6 +1,6 @@
services:
headscale:
image: headscale/headscale:latest
image: headscale/headscale:0.29.3
restart: unless-stopped
container_name: headscale-server
command: serve
@@ -53,7 +53,7 @@ services:
- "traefik.http.routers.headscale-metrics-dev.service=headscale-metrics"
- "traefik.http.routers.headscale-metrics-dev.tls=true"
headplane:
image: ghcr.io/tale/headplane:latest
image: ghcr.io/tale/headplane:0.7.1
container_name: headplane
restart: unless-stopped
ports:
@@ -65,8 +65,42 @@ services:
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
- proxy
labels:
- "traefik.enable=true"
- "traefik.http.services.headplane.loadbalancer.server.port=3000"
# Middleware: add /admin prefix for root requests
- "traefik.http.middlewares.headplane-prefix.addPrefix.prefix=/admin"
# Prod Root Router
- "traefik.http.routers.headplane-root.rule=Host(`hp.forust.xyz`)"
- "traefik.http.routers.headplane-root.entrypoints=websecure"
- "traefik.http.routers.headplane-root.middlewares=headplane-prefix"
- "traefik.http.routers.headplane-root.tls.certresolver=letsencrypt"
# Prod Router
- "traefik.http.routers.headplane.rule=Host(`hp.forust.xyz`) && PathPrefix(`/admin`)"
- "traefik.http.routers.headplane.entrypoints=websecure"
- "traefik.http.routers.headplane.tls.certresolver=letsencrypt"
# Local Root Router
- "traefik.http.routers.headplane-root-local.rule=Host(`hp.workstation.internal`)"
- "traefik.http.routers.headplane-root-local.entrypoints=websecure"
- "traefik.http.routers.headplane-root-local.middlewares=headplane-prefix"
- "traefik.http.routers.headplane-root-local.tls=true"
# Local Router
- "traefik.http.routers.headplane-local.rule=Host(`hp.workstation.internal`) && PathPrefix(`/admin`)"
- "traefik.http.routers.headplane-local.entrypoints=websecure"
- "traefik.http.routers.headplane-local.tls=true"
# Dev Root Router
- "traefik.http.routers.headplane-root-dev.rule=Host(`hp.gigaforust.internal`)"
- "traefik.http.routers.headplane-root-dev.entrypoints=websecure"
- "traefik.http.routers.headplane-root-dev.middlewares=headplane-prefix"
- "traefik.http.routers.headplane-root-dev.tls=true"
# Dev Router
- "traefik.http.routers.headplane-dev.rule=Host(`hp.gigaforust.internal`) && PathPrefix(`/admin`)"
- "traefik.http.routers.headplane-dev.entrypoints=websecure"
- "traefik.http.routers.headplane-dev.tls=true"
web:
image: goodieshq/headscale-admin:latest
image: goodieshq/headscale-admin:0.28.0
restart: unless-stopped
ports:
- 10080:80
+20 -26
View File
@@ -1,27 +1,21 @@
{
"groups": {
"group:admin": [
"admin@"
],
"group:users": []
},
"tagOwners": {},
"hosts": {},
"acls": [
{
"randomizeClientPort": false,
"#ha-meta": {
"name": "users",
"open": true
},
"action": "accept",
"src": [
"autogroup:member"
],
"dst": [
"autogroup:self:*"
]
}
],
"ssh": []
}
"groups": {
"group:admin": ["admin@"],
"group:users": []
},
"tagOwners": {},
"hosts": {},
"acls": [
{
"randomizeClientPort": false,
"#ha-meta": {
"name": "users",
"open": true
},
"action": "accept",
"src": ["autogroup:member"],
"dst": ["autogroup:self:*"]
}
],
"ssh": []
}
@@ -57,3 +57,30 @@ ports:
endpoints:
- addresses:
- "192.168.88.100"
---
apiVersion: v1
kind: Service
metadata:
name: headplane-external
namespace: headscale
spec:
ports:
- port: 3000
targetPort: 13000
name: http
---
apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
name: headplane-external
namespace: headscale
labels:
kubernetes.io/service-name: headplane-external
addressType: IPv4
ports:
- port: 13000
protocol: TCP
name: http
endpoints:
- addresses:
- "192.168.88.100"
@@ -1,7 +1,16 @@
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: headplane-prefix
namespace: headscale
spec:
addPrefix:
prefix: "/admin"
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: headscale-server-prod
name: headscale-prod
namespace: headscale
spec:
entryPoints:
@@ -9,18 +18,58 @@ spec:
routes:
- match: Host(`hs.forust.xyz`)
kind: Rule
middlewares:
- name: crowdsec-crowdsec-bouncer@kubernetescrd
services:
- name: headscale-server-external
port: 8080
- match: Host(`hs.forust.xyz`) && PathPrefix(`/admin`)
kind: Rule
middlewares:
- name: crowdsec-bouncer
namespace: crowdsec
services:
- name: headscale-ui-external
port: 80
- match: Host(`hs.forust.xyz`) && PathPrefix(`/metrics`)
kind: Rule
middlewares:
- name: crowdsec-bouncer
namespace: crowdsec
services:
- name: headscale-server-external
port: 9090
tls:
certResolver: letsencrypt
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: headscale-server-local
name: headplane-prod
namespace: headscale
spec:
entryPoints:
- websecure
routes:
- match: Host(`hp.forust.xyz`)
kind: Rule
middlewares:
- name: headplane-prefix
- name: crowdsec-bouncer
namespace: crowdsec
services:
- name: headplane-external
port: 3000
- match: Host(`hp.forust.xyz`) && PathPrefix(`/admin`)
kind: Rule
services:
- name: headplane-external
port: 3000
tls:
certResolver: letsencrypt
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: headscale-local
namespace: headscale
spec:
entryPoints:
@@ -31,76 +80,33 @@ spec:
services:
- name: headscale-server-external
port: 8080
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: headscale-ui-prod
namespace: headscale
spec:
entryPoints:
- websecure
routes:
- match: Host(`hs.forust.xyz`) && PathPrefix(`/admin`)
kind: Rule
middlewares:
- name: crowdsec-crowdsec-bouncer@kubernetescrd
- name: security-chain@file
services:
- name: headscale-ui-external
port: 80
tls:
certResolver: letsencrypt
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: headscale-ui-local
namespace: headscale
spec:
entryPoints:
- websecure
routes:
- match: HostRegexp(`^hs\.(workstation|gigaforust)\.internal$`) && PathPrefix(`/admin`)
- match: (Host(`hs.workstation.internal`) || Host(`hs.gigaforust.internal`)) && PathPrefix(`/admin`)
kind: Rule
services:
- name: headscale-ui-external
port: 80
- match: (Host(`hs.workstation.internal`) || Host(`hs.gigaforust.internal`)) && PathPrefix(`/metrics`)
kind: Rule
services:
- name: headscale-server-external
port: 9090
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: headscale-metrics-prod
name: headplane-local
namespace: headscale
spec:
entryPoints:
- websecure
routes:
- match: Host(`hs.forust.xyz`) && PathPrefix(`/metrics`)
- match: Host(`hp.workstation.internal`) || Host(`hp.gigaforust.internal`)
kind: Rule
middlewares:
- name: crowdsec-crowdsec-bouncer@kubernetescrd
- name: headplane-prefix
services:
- name: headscale-server-external
port: 9090
tls:
certResolver: letsencrypt
domains:
- main: hs.forust.xyz
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: headscale-metrics-local
namespace: headscale
spec:
entryPoints:
- websecure
routes:
- match: HostRegexp(`^hs\.(workstation|gigaforust)\.internal$`) && PathPrefix(`/metrics`)
- name: headplane-external
port: 3000
- match: (Host(`hp.workstation.internal`) || Host(`hp.gigaforust.internal`)) && PathPrefix(`/admin`)
kind: Rule
services:
- name: headscale-server-external
port: 9090
- name: headplane-external
port: 3000
Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 53 KiB

+206 -193
View File
@@ -1,212 +1,225 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>MrForust // XRock</title>
<script src="https://kit.fontawesome.com/a076d05399.js" crossorigin="anonymous"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<link rel="stylesheet" href="assets/css/style.css">
</head>
<body>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" />
<link rel="stylesheet" href="assets/css/style.css" />
</head>
<body>
<div class="container">
<header>
<h1 class="glitch" data-text="MrForust">Mr-Forust</h1>
<p class="subtitle">> CTF Player / XRock_Team / Just Signal.</p>
</header>
<header>
<h1 class="glitch" data-text="MrForust">Mr-Forust</h1>
<p class="subtitle">> CTF Player / XRock_Team / Just Signal.</p>
</header>
<hr>
<hr />
<div class="grid-2">
<section id="contacts">
<h2>./contacts</h2>
<ul class="link-list">
<li>
<i class="fab fa-github"></i>
<a href="https://github.com/mr-forust" target="_blank">github/mr-forust</a>
</li>
<li>
<i class="fas fa-flag"></i>
<a href="https://tryhackme.com/p/MrForust" target="_blank">tryhackme/MrForust</a>
</li>
<li>
<i class="fab fa-telegram-plane"></i>
<a href="https://t.me/MrForust" target="_blank">telegram/MrForust</a>
</li>
<li>
<i class="fab fa-discord"></i>
<span>discord/mr.forust</span>
</li>
<li>
<i class="fas fa-envelope"></i>
<a href="mailto:contact@forust.xyz">mail/contact@forust.xyz</a>
</li>
<li>
<i class="fa-solid fa-key"></i>
<a href=".well-known/pgp-key.asc">security/PGP Key</a>
</li>
<li>
<p class="comment"># PGP Key Fingerprint: A777 7CB7 D9C4 0A97 443D CCF0 7A3D A455 F820 5B82</p>
</li>
</ul>
</section>
<section id="tools">
<h2>./tools</h2>
<ul class="link-list">
<li>
<i class="fa fa-pie-chart"></i>
<a href="https://forust.xyz/glance" target="_blank">forust/dashboard</a>
<p class="comment"># Glance dashboard</p>
</li>
<li>
<i class="fa fa-refresh"></i>
<a href="https://forust.xyz/convert" target="_blank">forust/converter</a>
<p class="comment"># ConvertX instance</p>
</li>
<li>
<i class="fa-solid fa-file-pdf"></i>
<a href="https://pdf.forust.xyz" target="_blank">pdf.forust.xyz</a>
<p class="comment"># BentoPDF instance</p>
</li>
</ul>
</section>
</div>
<section id="stack">
<h2>./skills</h2>
<div class="grid-2">
<section id="socials">
<h2>./socials</h2>
<ul class="link-list">
<li>
<i class="fab fa-github"></i>
<a href="https://github.com/mr-forust" target="_blank">github/mr-forust</a>
</li>
<li>
<i class="fas fa-flag"></i>
<a href="https://tryhackme.com/p/MrForust" target="_blank">tryhackme/MrForust</a>
</li>
<li>
<i class="fab fa-telegram-plane"></i>
<a href="https://t.me/MrForust" target="_blank">telegram/MrForust</a>
</li>
<li>
<i class="fab fa-discord"></i>
<span>discord/mr.forust</span>
</li>
<li>
<i class="fas fa-envelope"></i>
<a href="mailto:contact@forust.xyz">mail/contact@forust.xyz</a>
</li>
<li>
<i class="fa-solid fa-key"></i>
<a href=".well-known/pgp-key.asc">security/PGP Key</a>
</li>
<li>
<p class="comment"># PGP Key Fingerprint: A777 7CB7 D9C4 0A97 443D CCF0 7A3D A455 F820 5B82</p>
</li>
</ul>
</section>
<section id="tools">
<h2>./tools</h2>
<ul class="link-list">
<li>
<i class="fa fa-pie-chart"></i>
<a href="https://forust.xyz/glance" target="_blank">forust/dashboard</a>
</li>
<li>
<i class="fa fa-refresh"></i>
<a href="https://forust.xyz/convert" target="_blank">forust/converter</a>
</li>
<li>
<i class="fa-solid fa-file-pdf"></i>
<a href="https://pdf.forust.xyz" target="_blank">pdf.forust.xyz</a>
</li>
</ul>
</section>
<div>
<div class="skill-item">
<span>ArchLinux # btw</span>
<span class="level">[#######...]</span>
</div>
<div class="skill-item"><span>Kubernetes</span> <span class="level">[###.......]</span></div>
<div class="skill-item"><span>Docker</span> <span class="level">[####......]</span></div>
<div class="skill-item">
<span>Docker Compose</span>
<span class="level">[#####.....]</span>
</div>
<div class="skill-item"><span>Web Pentest</span> <span class="level">[#####.....]</span></div>
<div class="skill-item"><span>Burpsuite</span> <span class="level">[#####.....]</span></div>
<div class="skill-item"><span>Steganography</span> <span class="level">[####......]</span></div>
</div>
<div>
<div class="skill-item"><span>Cryptography</span> <span class="level">[####......]</span></div>
<div class="skill-item"><span>OSINT</span> <span class="level">[####......]</span></div>
<div class="skill-item"><span>Python</span> <span class="level">[###.......]</span></div>
<div class="skill-item"><span>HTML</span> <span class="level">[###.......]</span></div>
<div class="skill-item"><span>Bash</span> <span class="level">[##........]</span></div>
<div class="skill-item"><span>Golang</span> <span class="level">[#.........]</span></div>
</div>
</div>
</section>
<section id="stack">
<h2>./skills</h2>
<div class="grid-2">
<div>
<div class="skill-item">
<span>ArchLinux # btw</span>
<span class="level">[#######...]</span>
</div>
<div class="skill-item">
<span>Kubernetes</span> <span class="level">[###.......]</span>
</div>
<div class="skill-item">
<span>Docker</span> <span class="level">[####......]</span>
</div>
<div class="skill-item">
<span>Docker Compose</span> <span class="level">[#####.....]</span>
</div>
<div class="skill-item">
<span>Web Pentest</span> <span class="level">[#####.....]</span>
</div>
<div class="skill-item">
<span>Burpsuite</span> <span class="level">[#####.....]</span>
</div>
<div class="skill-item">
<span>Steganography</span> <span class="level">[####......]</span>
</div>
</div>
<div>
<div class="skill-item">
<span>Cryptography</span> <span class="level">[####......]</span>
</div>
<div class="skill-item">
<span>OSINT</span> <span class="level">[####......]</span>
</div>
<div class="skill-item">
<span>Python</span> <span class="level">[###.......]</span>
</div>
<div class="skill-item">
<span>HTML</span> <span class="level">[###.......]</span>
</div>
<div class="skill-item">
<span>Bash</span> <span class="level">[##........]</span>
</div>
<div class="skill-item">
<span>Golang</span> <span class="level">[#.........]</span>
</div>
</div>
</div>
</section>
<section id="team">
<h2>./xrock_team</h2>
<p class="comment"># It's a select caste. Cybershamans. Cryptoanarchists. Shadows on the net..</p>
<section id="team">
<h2>./xrock_team</h2>
<p class="comment"># It's a select caste. Cybershamans. Cryptoanarchists. Shadows on the net..</p>
<div class="team-grid">
<div class="member">
<div
class="avatar"
style="
background-image: url(&quot;assets/images/team/mrforust.jpg&quot;);
background-size: cover;
background-position: center;
"
></div>
<a href="https://github.com/mr-forust" target="_blank">MrForust</a>
</div>
<div class="team-grid">
<div class="member">
<div class="avatar"
style="background-image: url('assets/images/team/mrforust.jpg'); background-size: cover; background-position: center;">
</div>
<a href="https://github.com/mr-forust" target="_blank">MrForust</a>
</div>
<div class="member">
<div
class="avatar"
style="
background-image: url(&quot;assets/images/team/anna.jpg&quot;);
background-size: cover;
background-position: center;
"
></div>
<a href="./assets/images/love.png" target="_blank">Anna~</a>
</div>
<div class="member">
<div class="avatar"
style="background-image: url('assets/images/team/anna.jpg'); background-size: cover; background-position: center;">
</div>
<a href="./assets/images/love.png" target="_blank">Anna~</a>
</div>
<div class="member">
<div
class="avatar"
style="
background-image: url(&quot;assets/images/team/chernuha.jpg&quot;);
background-size: cover;
background-position: center;
"
></div>
<a href="https://chernuha.space" target="_blank">Chernuha</a>
</div>
<div class="member">
<div class="avatar"
style="background-image: url('assets/images/team/chernuha.jpg'); background-size: cover; background-position: center;">
</div>
<a href="https://chernuha.space" target="_blank">Chernuha</a>
</div>
<div class="member">
<div
class="avatar"
style="
background-image: url(&quot;assets/images/team/hudan.jpg&quot;);
background-size: cover;
background-position: center;
"
></div>
<a href="https://hudan.xyz" target="_blank">p1ngvi</a>
</div>
<div class="member">
<div class="avatar"
style="background-image: url('assets/images/team/hudan.jpg'); background-size: cover; background-position: center;">
</div>
<a href="https://hudan.xyz" target="_blank">p1ngvi</a>
</div>
<div class="member">
<div
class="avatar"
style="
background-image: url(&quot;assets/images/team/xdfnx.jpg&quot;);
background-size: cover;
background-position: center;
"
></div>
<a href="https://xdfnx.cfd" target="_blank">xdfnx</a>
</div>
</div>
</section>
<div class="member">
<div class="avatar"
style="background-image: url('assets/images/team/xdfnx.jpg'); background-size: cover; background-position: center;">
</div>
<a href="https://xdfnx.cfd" target="_blank">xdfnx</a>
</div>
</div>
</section>
<section id="projects">
<h2>./projects</h2>
<ul class="repo-list">
<li>
<a href="https://gitea.forust.xyz/forust/gosleep" target="_blank">forust/gosleep</a>
<span class="comment">// linux sleep timer written in rust (originally in go)</span>
</li>
</ul>
</section>
<section id="repos">
<h2>./favorite_repos</h2>
<ul class="repo-list">
<li>
<a href="https://github.com/TDesktop-x64/tdesktop" target="_blank">TDesktop-x64/tdesktop</a>
<span class="comment">// unofficial telegram client with some additions</span>
</li>
<li>
<a href="https://github.com/traefik/traefik" target="_blank">traefik/traefik</a>
<span class="comment">// beloved reverse-proxy</span>
</li>
<li>
<a href="https://github.com/unhappychoice/gitlogue" target="_blank">unhappychoice/gitlogue</a>
<span class="comment">// nice git log visualizer</span>
</li>
<li>
<a href="https://github.com/tstack/lnav" target="_blank">tstack/lnav</a>
<span class="comment">// powerful log reader</span>
</li>
<li>
<a href="https://github.com/mountain-loop/yaak" target="_blank">mountain-loop/yaak</a>
<span class="comment">// modern, fancy api client</span>
</li>
<li>
<a href="https://github.com/pear-devs/pear-desktop" target="_blank">pear-devs/pear-desktop</a>
<span class="comment">// music client with a lot of features</span>
</li>
<li>
<a href="https://github.com/epi052/feroxbuster" target="_blank">epi052/feroxbuster</a>
<span class="comment">// directory discovery</span>
</li>
</ul>
</section>
<section id="repos">
<h2>./favorite_repos</h2>
<ul class="repo-list">
<li>
<a href="https://github.com/TDesktop-x64/tdesktop" target="_blank">TDesktop-x64/tdesktop</a>
<span class="comment">// unofficial telegram client with some additions</span>
</li>
<li>
<a href="https://github.com/traefik/traefik" target="_blank">traefik/traefik</a>
<span class="comment">// beloved reverse-proxy</span>
</li>
<li>
<a href="https://github.com/unhappychoice/gitlogue" target="_blank">unhappychoice/gitlogue</a>
<span class="comment">// nice git log visualizer</span>
</li>
<li>
<a href="https://github.com/tstack/lnav" target="_blank">tstack/lnav</a>
<span class="comment">// powerful log reader</span>
</li>
<li>
<a href="https://github.com/mountain-loop/yaak" target="_blank">mountain-loop/yaak</a>
<span class="comment">// modern, fancy api client</span>
</li>
<li>
<a href="https://github.com/pear-devs/pear-desktop" target="_blank">pear-devs/pear-desktop</a>
<span class="comment">// music client with a lot of features</span>
</li>
<li>
<a href="https://github.com/epi052/feroxbuster" target="_blank">epi052/feroxbuster</a>
<span class="comment">// directory discovery</span>
</li>
</ul>
</section>
<footer>
<p>root@xrock:~$ cat <a href="miku.html">./miku</a></p>
<p>miku?</p>
<p>root@xrock:~$ shutdown -h now</p>
<p>&copy; XRock - Just Signal.</p>
</footer>
<footer>
<p>root@xrock:~$ cat <a href="miku.html">./miku</a></p>
<p>miku?</p>
<p>root@xrock:~$ shutdown -h now</p>
<p>&copy; XRock - Just Signal.</p>
</footer>
</div>
</body>
</html>
</body>
</html>
View File
Whitespace-only changes.
+5 -3
View File
@@ -10,7 +10,8 @@ spec:
- match: Host(`forust.xyz`) || Host(`www.forust.xyz`)
kind: Rule
middlewares:
- name: crowdsec-crowdsec-bouncer@kubernetescrd
- name: crowdsec-bouncer
namespace: crowdsec
priority: 10
services:
- name: forust-homepage-service
@@ -43,10 +44,11 @@ spec:
entryPoints:
- websecure
routes:
- match: Host(`xdfnx.cfd`) || Host(`www.xdfnx.cfd`)
- match: Host(`xdfnx.cfd`)
kind: Rule
middlewares:
- name: crowdsec-crowdsec-bouncer@kubernetescrd
- name: crowdsec-bouncer
namespace: crowdsec
services:
- name: xdfnx-homepage-service
port: 80
File diff suppressed because it is too large. Load diff
+2 -2
View File
@@ -1,6 +1,6 @@
services:
kener:
image: rajnandan1/kener:4.0.23
image: rajnandan1/kener:4.1.5
container_name: kener
restart: unless-stopped
# ports:
@@ -36,7 +36,7 @@ services:
- proxy
- kener
redis:
image: redis:7-alpine
image: redis:7.4.11-alpine
container_name: kener-redis
restart: unless-stopped
volumes:
+2 -1
View File
@@ -10,7 +10,8 @@ spec:
- match: Host(`status.forust.xyz`)
kind: Rule
middlewares:
- name: crowdsec-crowdsec-bouncer@kubernetescrd
- name: crowdsec-bouncer
namespace: crowdsec
services:
- name: kener-service
port: 3000
+1 -1
View File
@@ -27,7 +27,7 @@ spec:
spec:
containers:
- name: kener
image: rajnandan1/kener:4.1.0
image: rajnandan1/kener:4.1.5
envFrom:
- configMapRef:
name: kener-config
+1 -1
View File
@@ -30,7 +30,7 @@ spec:
spec:
containers:
- name: redis
image: redis:7-alpine
image: redis:7.4.11-alpine
ports:
- containerPort: 6379
volumeMounts:
+1 -1
View File
@@ -1,6 +1,6 @@
services:
metube:
image: ghcr.io/alexta69/metube
image: ghcr.io/alexta69/metube:2026.08.28
container_name: metube
restart: unless-stopped
# ports:
View File
Whitespace-only changes.
+1 -1
View File
@@ -27,7 +27,7 @@ spec:
spec:
containers:
- name: metube
image: ghcr.io/alexta69/metube
image: ghcr.io/alexta69/metube:2026.08.28
envFrom:
- configMapRef:
name: metube-config
+1 -1
View File
@@ -1,6 +1,6 @@
services:
n8n:
image: docker.n8n.io/n8nio/n8n
image: docker.n8n.io/n8nio/n8n:2.38.7
container_name: n8n
restart: unless-stopped
environment:
View File
Whitespace-only changes.
+2 -1
View File
@@ -10,7 +10,8 @@ spec:
- match: Host(`n8n.forust.xyz`)
kind: Rule
middlewares:
- name: "crowdsec-crowdsec-bouncer@kubernetescrd"
- name: crowdsec-bouncer
namespace: crowdsec
services:
- name: n8n-service
port: 5678
+1 -1
View File
@@ -27,7 +27,7 @@ spec:
spec:
containers:
- name: n8n
image: docker.n8n.io/n8nio/n8n
image: docker.n8n.io/n8nio/n8n:2.38.7
envFrom:
- configMapRef:
name: n8n-config
+2 -2
View File
@@ -1,6 +1,6 @@
services:
netronome:
image: ghcr.io/autobrr/netronome:latest
image: ghcr.io/autobrr/netronome:v0.14.0
restart: unless-stopped
container_name: netronome
ports:
@@ -33,7 +33,7 @@ services:
condition: service_healthy
postgres:
container_name: netronome-postgres
image: postgres:17-alpine
image: postgres:17.11-alpine
environment:
- POSTGRES_USER=netronome
- POSTGRES_PASSWORD=netronome
View File
Whitespace-only changes.
+1 -1
View File
@@ -5,7 +5,7 @@ metadata:
namespace: netronome
data:
NETRONOME__DB_TYPE: "postgres"
NETRONOME__DB_HOST: "netronome-postgres-service"
NETRONOME__DB_HOST: "postgres.database.svc.cluster.local"
NETRONOME__DB_PORT: "5432"
NETRONOME__DB_NAME: "netronome"
NETRONOME__DB_SSLMODE: "disable"
+2 -1
View File
@@ -10,7 +10,8 @@ spec:
- match: Host(`nm.forust.xyz`)
kind: Rule
middlewares:
- name: "crowdsec-crowdsec-bouncer@kubernetescrd"
- name: crowdsec-bouncer
namespace: crowdsec
services:
- name: netronome-service
port: 7575
+1 -1
View File
@@ -30,7 +30,7 @@ spec:
spec:
containers:
- name: netronome
image: ghcr.io/autobrr/netronome:latest
image: ghcr.io/autobrr/netronome:v0.14.0
ports:
- name: netronome-port
protocol: TCP
-71
View File
@@ -1,71 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: netronome-postgres-service
namespace: netronome
spec:
selector:
app: netronome-postgres
ports:
- protocol: TCP
port: 5432
targetPort: 5432
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: netronome-postgres
namespace: netronome
spec:
serviceName: "netronome-postgres-service"
replicas: 1
selector:
matchLabels:
app: netronome-postgres
template:
metadata:
labels:
app: netronome-postgres
spec:
containers:
- name: netronome-postgres
image: postgres:17-alpine
ports:
- name: postgres-port
protocol: TCP
containerPort: 5432
env:
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: netronome-secrets
key: NETRONOME__DB_USER
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: netronome-secrets
key: NETRONOME__DB_PASSWORD
- name: POSTGRES_DB
valueFrom:
configMapKeyRef:
name: netronome-config
key: NETRONOME__DB_NAME
resources:
requests:
memory: "512Mi"
cpu: "500m"
limits:
memory: "1Gi"
cpu: "1000m"
volumeMounts:
- name: netronome-pg-data
mountPath: /var/lib/postgresql/data
volumeClaimTemplates:
- metadata:
name: netronome-pg-data
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
@@ -12,7 +12,8 @@ spec:
kind: Rule
middlewares:
- name: nextcloud-chain@file
- name: crowdsec-crowdsec-bouncer@kubernetescrd
- name: crowdsec-bouncer
namespace: crowdsec
services:
- name: nextcloud-apache
port: 11000
@@ -31,7 +32,8 @@ spec:
- match: Host(`nextcloud.workstation.internal`) || Host(`nextcloud.gigaforust.internal`)
kind: Rule
middlewares:
- name: "crowdsec-crowdsec-bouncer@kubernetescrd"
- name: crowdsec-bouncer
namespace: crowdsec
- name: nextcloud-chain@file
services:
- name: nextcloud-apache
+5 -5
View File
@@ -84,7 +84,7 @@ services:
# - "443:443"
penpot-frontend:
image: "penpotapp/frontend:${PENPOT_VERSION:-latest}"
image: "penpotapp/frontend:${PENPOT_VERSION:-2.17.2}"
restart: always
# ports:
# - 9001:8080
@@ -119,7 +119,7 @@ services:
environment:
<<: [*penpot-flags, *penpot-http-body-size]
penpot-backend:
image: "penpotapp/backend:${PENPOT_VERSION:-latest}"
image: "penpotapp/backend:${PENPOT_VERSION:-2.17.2}"
restart: always
volumes:
@@ -189,7 +189,7 @@ services:
# PENPOT_SMTP_SSL: false
penpot-exporter:
image: "penpotapp/exporter:${PENPOT_VERSION:-latest}"
image: "penpotapp/exporter:${PENPOT_VERSION:-2.17.2}"
restart: always
depends_on:
@@ -209,7 +209,7 @@ services:
PENPOT_REDIS_URI: redis://penpot-valkey/0
penpot-postgres:
image: "postgres:15"
image: "postgres:15.19-alpine"
restart: always
stop_signal: SIGINT
@@ -233,7 +233,7 @@ services:
- POSTGRES_PASSWORD=penpot
penpot-valkey:
image: valkey/valkey:8.1
image: valkey/valkey:8.1.10
restart: always
healthcheck:
+1 -1
View File
@@ -1,6 +1,6 @@
services:
portainer:
image: portainer/portainer-ce:2.41.0
image: portainer/portainer-ce:2.45.0
container_name: portainer
restart: always
volumes:
View File
Whitespace-only changes.
+2 -1
View File
@@ -10,7 +10,8 @@ spec:
- match: Host(`portainer.forust.xyz`)
kind: Rule
middlewares:
- name: "crowdsec-crowdsec-bouncer@kubernetescrd"
- name: crowdsec-bouncer
namespace: crowdsec
services:
- name: portainer-service
port: 9000
+1 -1
View File
@@ -27,7 +27,7 @@ spec:
spec:
containers:
- name: portainer
image: portainer/portainer-ce:2.41.0
image: portainer/portainer-ce:2.45.0
ports:
- containerPort: 9000
volumeMounts:
Loaded 100 of 266 files, more files were not shown because too many files have changed in this diff. Show more