CI / Build and push docker image (push) Successful in 2m27s
kaya-oidc 0.0.6 can run several providers in one app (OIDCProviders), each with its own login route and namespaced session state. Adopt it: providers are configured in indexed, contiguous OIDC_<n>_* blocks (0-based) with OIDC_<n>_PROVIDER naming the provider in its login URL and session keys. The old unindexed OIDC_ISSUER/OIDC_CLIENT_ID/... names are no longer read; startup fails with a rename hint when they are set. With no OIDC variables at all, the single dev mock provider named "default" is used, as before. The platform now authenticates through OIDCProviders: auth.current_user returns the session's user (sessions stay single-identity, so at most one provider is logged in) and require_auth/route handlers use it, while the websocket path keeps scanning for any provider's user. New routes expose the configuration to the SPA and keep old links working: GET /api/auth/providers lists the login URLs, GET /auth/login redirects to the first provider and GET /auth/logout to whichever provider the session is logged in with. The lobby renders one login button per provider from that endpoint. Update the env examples, compose and k8s manifests to the indexed scheme, document the multiple-provider setup and the migration, and cover it with config, auth and route tests.
319 lines
9.8 KiB
YAML
319 lines
9.8 KiB
YAML
# Tavolo — Kubernetes deployment (namespace: tavolo)
|
|
#
|
|
# Single apply:
|
|
# kubectl apply -f deploy/k8s/tavolo.yaml
|
|
#
|
|
# Replace every value marked REPLACE_ME before applying.
|
|
#
|
|
# Scope:
|
|
# - Postgres and the OIDC provider are NOT part of this file; they are
|
|
# deployed in other namespaces and referenced by DNS name.
|
|
# - Redis IS included (ephemeral: sessions and live games are disposable,
|
|
# mirroring docker-compose's no-volume choice).
|
|
#
|
|
# 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 Postgres NetworkPolicy (if any) must allow ingress from the
|
|
# tavolo namespace.
|
|
# - This file deploys a ClusterIP Service only. Until you add an Ingress,
|
|
# reach the app with:
|
|
# kubectl port-forward -n tavolo svc/tavolo 8080:80
|
|
# and set OIDC_0_REDIRECT_URI to match whatever URL the browser uses.
|
|
|
|
---
|
|
apiVersion: v1
|
|
kind: Namespace
|
|
metadata:
|
|
name: tavolo
|
|
labels:
|
|
app.kubernetes.io/name: tavolo
|
|
app.kubernetes.io/part-of: tavolo
|
|
|
|
---
|
|
apiVersion: v1
|
|
kind: ConfigMap
|
|
metadata:
|
|
name: tavolo-config
|
|
namespace: tavolo
|
|
labels:
|
|
app.kubernetes.io/name: tavolo
|
|
app.kubernetes.io/component: app
|
|
app.kubernetes.io/part-of: tavolo
|
|
data:
|
|
# In-cluster Redis deployed by this file.
|
|
REDIS_URL: redis://redis.tavolo.svc.cluster.local:6379/0
|
|
# Postgres (external, lives in another namespace): everything except the
|
|
# password, which is the only entry in the tavolo-secrets Secret.
|
|
# Use its Service DNS name, e.g. postgres.<namespace>.svc.cluster.local.
|
|
DATABASE_ENGINE: postgres
|
|
DATABASE_HOST: REPLACE_ME
|
|
DATABASE_PORT: "5432"
|
|
DATABASE_NAME: REPLACE_ME
|
|
DATABASE_USER: REPLACE_ME
|
|
# Extra DSN query parameters appended to the URL (e.g. ssl=require).
|
|
# Empty means none.
|
|
DATABASE_OPTIONS: ""
|
|
# The image bakes STATIC_DIR=/app/web/dist; repeat it here for clarity.
|
|
STATIC_DIR: /app/web/dist
|
|
GAME_TTL_SECONDS: "86400"
|
|
HAND_ACK_TIMEOUT_SECONDS: "30"
|
|
TURN_TIMEOUT_SECONDS: "30"
|
|
# CORS (kaya-cors' CorsMixin). 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 "*"
|
|
# CORS_ALLOW_ORIGIN_REGEX: 'https://tavolo-[a-z0-9-]+\.vercel\.app'
|
|
# CORS_ALLOW_METHODS: "GET,POST" # default: GET; "*" = all
|
|
# CORS_ALLOW_HEADERS: "Authorization,Content-Type" # "*" mirrors the request
|
|
# 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.
|
|
# OTEL_ENABLED: "true"
|
|
# OTEL_SERVICE_NAME: "tavolo"
|
|
# OTEL_EXPORTER_OTLP_ENDPOINT: "http://otel-collector.observability:4318"
|
|
# OTEL_EXPORTER_OTLP_HEADERS: "Authorization=Bearer ..."
|
|
# OTEL_EXCLUDED_PATHS: "/api/health" # default; paths skipped by tracing
|
|
# OTEL_METRIC_EXPORT_INTERVAL_MILLIS: "60000"
|
|
# OTEL_METRICS_POLL_INTERVAL_SECONDS: "15"
|
|
# OIDC (the provider lives in another namespace). Each provider is an
|
|
# indexed block: OIDC_<n>_PROVIDER names it in the login URL
|
|
# (/auth/<provider>/login) and indices must be contiguous from 0. Add
|
|
# OIDC_1_*, OIDC_2_*, ... blocks for more providers; add their secrets
|
|
# to tavolo-secrets as well.
|
|
OIDC_0_PROVIDER: default
|
|
OIDC_0_CLIENT_ID: tavolo
|
|
OIDC_0_POST_LOGIN_REDIRECT: /
|
|
OIDC_0_POST_LOGOUT_REDIRECT: /
|
|
# Issuer URL as seen by the browser AND the pods (see notes above).
|
|
OIDC_0_ISSUER: REPLACE_ME
|
|
# Public callback URL of this deployment, e.g.
|
|
# http://<host>:<port>/auth/default/callback (must be allowed at the
|
|
# provider). When unset it defaults to
|
|
# http://localhost:8080/auth/<provider>/callback.
|
|
OIDC_0_REDIRECT_URI: REPLACE_ME
|
|
|
|
---
|
|
apiVersion: v1
|
|
kind: Secret
|
|
metadata:
|
|
name: tavolo-secrets
|
|
namespace: tavolo
|
|
labels:
|
|
app.kubernetes.io/name: tavolo
|
|
app.kubernetes.io/component: app
|
|
app.kubernetes.io/part-of: tavolo
|
|
type: Opaque
|
|
stringData:
|
|
# The only Postgres secret: the password for DATABASE_USER at
|
|
# DATABASE_HOST (both configured in the tavolo-config ConfigMap).
|
|
DATABASE_PASSWORD: REPLACE_ME
|
|
# Client secret for OIDC_0_CLIENT_ID at the provider.
|
|
OIDC_0_CLIENT_SECRET: REPLACE_ME
|
|
|
|
---
|
|
# Redis: sessions + live game state. Ephemeral by design (emptyDir): worst
|
|
# case after a restart users log in again and games in progress expire.
|
|
apiVersion: apps/v1
|
|
kind: Deployment
|
|
metadata:
|
|
name: redis
|
|
namespace: tavolo
|
|
labels:
|
|
app.kubernetes.io/name: tavolo
|
|
app.kubernetes.io/component: redis
|
|
app.kubernetes.io/part-of: tavolo
|
|
spec:
|
|
replicas: 1
|
|
# Redis is a singleton here; avoid a window with two masters on rollout.
|
|
strategy:
|
|
type: Recreate
|
|
selector:
|
|
matchLabels:
|
|
app.kubernetes.io/name: tavolo
|
|
app.kubernetes.io/component: redis
|
|
template:
|
|
metadata:
|
|
labels:
|
|
app.kubernetes.io/name: tavolo
|
|
app.kubernetes.io/component: redis
|
|
app.kubernetes.io/part-of: tavolo
|
|
spec:
|
|
securityContext:
|
|
runAsNonRoot: true
|
|
runAsUser: 999 # redis user in the official image
|
|
fsGroup: 999
|
|
containers:
|
|
- name: redis
|
|
image: redis:8-alpine
|
|
# Snapshots would only live on an emptyDir anyway: skip them.
|
|
args: ["--save", "", "--appendonly", "no"]
|
|
ports:
|
|
- name: redis
|
|
containerPort: 6379
|
|
volumeMounts:
|
|
- name: data
|
|
mountPath: /data
|
|
readinessProbe:
|
|
exec:
|
|
command: ["redis-cli", "ping"]
|
|
periodSeconds: 5
|
|
livenessProbe:
|
|
exec:
|
|
command: ["redis-cli", "ping"]
|
|
periodSeconds: 10
|
|
resources:
|
|
requests:
|
|
cpu: 25m
|
|
memory: 64Mi
|
|
limits:
|
|
cpu: 250m
|
|
memory: 128Mi
|
|
volumes:
|
|
- name: data
|
|
emptyDir: {}
|
|
|
|
---
|
|
apiVersion: v1
|
|
kind: Service
|
|
metadata:
|
|
name: redis
|
|
namespace: tavolo
|
|
labels:
|
|
app.kubernetes.io/name: tavolo
|
|
app.kubernetes.io/component: redis
|
|
app.kubernetes.io/part-of: tavolo
|
|
spec:
|
|
type: ClusterIP
|
|
selector:
|
|
app.kubernetes.io/name: tavolo
|
|
app.kubernetes.io/component: redis
|
|
ports:
|
|
- name: redis
|
|
port: 6379
|
|
targetPort: redis
|
|
|
|
---
|
|
# The app (backend + compiled SPA + WebSocket endpoint, one image).
|
|
# The "migrate" initContainer runs `aerich upgrade` 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.
|
|
apiVersion: apps/v1
|
|
kind: Deployment
|
|
metadata:
|
|
name: tavolo
|
|
namespace: tavolo
|
|
labels:
|
|
app.kubernetes.io/name: tavolo
|
|
app.kubernetes.io/component: app
|
|
app.kubernetes.io/part-of: tavolo
|
|
spec:
|
|
replicas: 1
|
|
selector:
|
|
matchLabels:
|
|
app.kubernetes.io/name: tavolo
|
|
app.kubernetes.io/component: app
|
|
template:
|
|
metadata:
|
|
labels:
|
|
app.kubernetes.io/name: tavolo
|
|
app.kubernetes.io/component: app
|
|
app.kubernetes.io/part-of: tavolo
|
|
spec:
|
|
# Let WebSocket connections drain on rollout.
|
|
terminationGracePeriodSeconds: 30
|
|
securityContext:
|
|
runAsNonRoot: true
|
|
initContainers:
|
|
- name: migrate
|
|
image: gitea.woggioni.net/woggioni/tavolo:latest
|
|
# aerich reads [tool.aerich] from pyproject.toml in /app.
|
|
command: ["aerich", "upgrade"]
|
|
workingDir: /app
|
|
envFrom:
|
|
# Migrations need the non-secret DATABASE_* parts too.
|
|
- configMapRef:
|
|
name: tavolo-config
|
|
- secretRef:
|
|
name: tavolo-secrets
|
|
resources:
|
|
requests:
|
|
cpu: 100m
|
|
memory: 128Mi
|
|
limits:
|
|
cpu: 500m
|
|
memory: 256Mi
|
|
containers:
|
|
- name: tavolo
|
|
# Pin a release/* tag for production instead of :latest.
|
|
image: gitea.woggioni.net/woggioni/tavolo:latest
|
|
ports:
|
|
- name: http
|
|
containerPort: 8080
|
|
envFrom:
|
|
- configMapRef:
|
|
name: tavolo-config
|
|
- secretRef:
|
|
name: tavolo-secrets
|
|
securityContext:
|
|
allowPrivilegeEscalation: false
|
|
capabilities:
|
|
drop: ["ALL"]
|
|
readOnlyRootFilesystem: true
|
|
volumeMounts:
|
|
- name: tmp
|
|
mountPath: /tmp
|
|
startupProbe:
|
|
httpGet:
|
|
path: /api/health
|
|
port: http
|
|
periodSeconds: 2
|
|
failureThreshold: 30
|
|
readinessProbe:
|
|
httpGet:
|
|
path: /api/health
|
|
port: http
|
|
periodSeconds: 10
|
|
livenessProbe:
|
|
httpGet:
|
|
path: /api/health
|
|
port: http
|
|
periodSeconds: 10
|
|
resources:
|
|
requests:
|
|
cpu: 100m
|
|
memory: 128Mi
|
|
limits:
|
|
cpu: 500m
|
|
memory: 512Mi
|
|
volumes:
|
|
- name: tmp
|
|
emptyDir: {}
|
|
|
|
---
|
|
apiVersion: v1
|
|
kind: Service
|
|
metadata:
|
|
name: tavolo
|
|
namespace: tavolo
|
|
labels:
|
|
app.kubernetes.io/name: tavolo
|
|
app.kubernetes.io/component: app
|
|
app.kubernetes.io/part-of: tavolo
|
|
spec:
|
|
type: ClusterIP
|
|
# No session affinity needed: sessions and live games live in Redis.
|
|
selector:
|
|
app.kubernetes.io/name: tavolo
|
|
app.kubernetes.io/component: app
|
|
ports:
|
|
- name: http
|
|
port: 80
|
|
targetPort: http
|