Rewrite the backend in Rust with Axum
CI / Build and push docker image (push) Successful in 4m20s

Replace the Python/kaya server in server/ with a Rust Cargo workspace of
three crates mirroring the old layout: tavolo-platform (lobby API, live-game
store, websocket endpoint, deadline scheduler, stats/Elo, OIDC sessions),
tavolo-scopone (pure scopone scientifico rules plus the platform adapter)
and tavolo-app (composition root and the `tavolo` binary with
serve/migrate/backfill-elo).

The HTTP and WebSocket contracts, error messages, Redis key layout and
Postgres schema are unchanged, so the existing web/ frontend and in-flight
games keep working. Notable differences: sessions are JSON under
tavolo:session: (Python pickle sessions invalidate once), LOGGING_CONFIG is
ignored in favour of RUST_LOG, DATABASE_URL must be Postgres, sqlx migrations
replace aerich, and OpenTelemetry exports Rust process metrics instead of
CPython gc metrics (no Postgres/Redis client instrumentation).

Docker Compose and the Kubernetes manifest now run the single Rust image
(embedded migrations, /static and /assets served by the app); the release CI
keeps building server/Dockerfile. The full test suite (scopone rules,
platform store/deadlines/stats/websockets, OIDC login flow, scopone e2e) is
ported and runs with cargo test --workspace; Redis/Postgres integration tests
are gated on TEST_REDIS_URL/TEST_DATABASE_URL.
This commit is contained in:
2026-10-02 13:54:55 +00:00
parent adc3d340b1
commit 7b3b38ccbc
136 changed files with 16821 additions and 11777 deletions
+12 -11
View File
@@ -13,9 +13,10 @@
#
# Reachability notes:
# - The OIDC issuer URL must resolve from the browser AND from the pods
# (kaya does discovery lazily on the first login, so startup succeeds
# even when the issuer is unreachable, but logins then fail). If the
# provider is internal-only, you need split DNS or a public issuer URL.
# (the server discovers the provider lazily on the first login, so
# startup succeeds even when the issuer is unreachable, but logins then
# fail). If the provider is internal-only, you need split DNS or a public
# issuer URL.
# - The Postgres NetworkPolicy (if any) must allow ingress from the
# tavolo namespace.
# - This file deploys a ClusterIP Service only. Until you add an Ingress,
@@ -61,7 +62,8 @@ data:
GAME_TTL_SECONDS: "86400"
HAND_ACK_TIMEOUT_SECONDS: "30"
TURN_TIMEOUT_SECONDS: "30"
# CORS (kaya-cors' CorsMixin). Disabled unless CORS_ALLOW_ORIGINS or
# CORS (Starlette-compatible middleware). Disabled unless
# CORS_ALLOW_ORIGINS or
# CORS_ALLOW_ORIGIN_REGEX is set — unneeded when the SPA and the API are
# served from the same origin. See server/.env.example for details.
# CORS_ALLOW_ORIGINS: "https://example.com,https://app.example.com" # or "*"
@@ -71,9 +73,9 @@ data:
# CORS_ALLOW_CREDENTIALS: "false"
# CORS_EXPOSE_HEADERS: ""
# CORS_MAX_AGE: "600"
# OpenTelemetry (kaya-otel + contrib instrumentations): traces
# (HTTP/WebSocket, Postgres, Redis), metrics and logs via OTLP/HTTP,
# disabled unless OTEL_ENABLED is set. The image bundles the otel extra.
# OpenTelemetry (compiled into the image): traces (HTTP/WebSocket),
# process + tavolo metrics and logs via OTLP/HTTP, disabled unless
# OTEL_ENABLED is set.
# OTEL_ENABLED: "true"
# OTEL_SERVICE_NAME: "tavolo"
# OTEL_EXPORTER_OTLP_ENDPOINT: "http://otel-collector.observability:4318"
@@ -200,7 +202,7 @@ spec:
---
# The app (backend + compiled SPA + WebSocket endpoint, one image).
# The "migrate" initContainer runs `aerich upgrade` before the app starts,
# The "migrate" initContainer runs `tavolo migrate` before the app starts,
# so this single file can be applied safely in any order. Migrations are
# idempotent, but if you ever scale above 1 replica, move this to a Job to
# avoid concurrent upgrades.
@@ -233,9 +235,8 @@ spec:
initContainers:
- name: migrate
image: gitea.woggioni.net/woggioni/tavolo:latest
# aerich reads [tool.aerich] from pyproject.toml in /app.
command: ["aerich", "upgrade"]
workingDir: /app
# sqlx migrations are embedded in the binary.
command: ["tavolo", "migrate"]
envFrom:
# Migrations need the non-secret DATABASE_* parts too.
- configMapRef: