diff --git a/workout-system/sparkyfitness.yaml b/workout-system/sparkyfitness.yaml new file mode 100644 index 0000000..89de147 --- /dev/null +++ b/workout-system/sparkyfitness.yaml @@ -0,0 +1,496 @@ +# SparkyFitness - Workout / nutrition tracker +# https://github.com/CodeWithCJ/SparkyFitness +# Version: v0.16.6.3 (pinned) +# Domain: workout.dooplex.hu (+ workout.home) +# Auth: SparkyFitness NATIVE OIDC against Authentik (no forward-auth in front). +# DB: dedicated postgres:15-alpine in this namespace (NOT the shared CNPG cluster). +# +# ============================================================================ +# Replaces wger (see workout.yaml — wger is parked, not deleted, for rollback). +# +# Image names: the upstream Helm chart uses codewithcj/sparkyfitness-{server,frontend}; +# we use the docker-compose image names (codewithcj/sparkyfitness_server and +# codewithcj/sparkyfitness) as specified for this deployment. The frontend root +# image's nginx has `listen 80;` hardcoded, so the frontend listens on :80 +# (NGINX_LISTEN_PORT set explicitly to match container/Service ports). +# +# Secrets (manual, NOT in git — created with kubectl): +# sparky-oauth : client-id, client-secret (OIDC; already created) +# sparky-app : db-user, db-password, app-db-user, (DB owner + app user + +# app-db-password, api-encryption-key, crypto keys) +# better-auth-secret +# api-encryption-key / better-auth-secret must NEVER change after first boot +# (changing them locks out 2FA users and makes encrypted data unrecoverable). +# +# Authentik provider: OAuth2/OpenID, application slug `sparkyfitness`, confidential, +# RS256, scopes `openid profile email`. Issuer must match SPARKY_FITNESS_OIDC_ISSUER_URL +# below. Register the exact oidc-callback redirect URI shown in SparkyFitness admin +# (Settings -> Authentication) once the app is up. +# ============================================================================ +--- +# ---------------------------------------------------------------------------- +# Database: dedicated PostgreSQL 15 (alpine). POSTGRES_USER is the cluster +# superuser, so SparkyFitness auto-creates the limited app role, applies RLS +# table ownership, and installs uuid-ossp/pgcrypto/pg_stat_statements on its +# own — no init SQL or shared_preload_libraries tinkering needed. +# ---------------------------------------------------------------------------- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: sparkyfitness-db + namespace: workout-system + labels: + app.kubernetes.io/instance: sparkyfitness + app.kubernetes.io/name: postgres +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + app.kubernetes.io/instance: sparkyfitness + app.kubernetes.io/name: postgres + template: + metadata: + labels: + app.kubernetes.io/instance: sparkyfitness + app.kubernetes.io/name: postgres + spec: + enableServiceLinks: false + securityContext: + fsGroup: 999 + containers: + - name: postgres + image: postgres:15-alpine + env: + - name: POSTGRES_USER + valueFrom: + secretKeyRef: + name: sparky-app + key: db-user + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: sparky-app + key: db-password + - name: POSTGRES_DB + value: "sparkyfitness_db" + # Keep PG data in a subdir so the volume mount root can hold lost+found + - name: PGDATA + value: "/var/lib/postgresql/data/pgdata" + ports: + - containerPort: 5432 + name: postgres + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + cpu: 500m + memory: 512Mi + volumeMounts: + - name: data + mountPath: /var/lib/postgresql/data + subPath: data + livenessProbe: + exec: + command: ["sh", "-c", "pg_isready -U \"$POSTGRES_USER\" -d \"$POSTGRES_DB\""] + initialDelaySeconds: 30 + periodSeconds: 15 + timeoutSeconds: 5 + failureThreshold: 5 + readinessProbe: + exec: + command: ["sh", "-c", "pg_isready -U \"$POSTGRES_USER\" -d \"$POSTGRES_DB\""] + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 5 + volumes: + - name: data + persistentVolumeClaim: + claimName: sparkyfitness-postgres +--- +# ---------------------------------------------------------------------------- +# Server: Node.js backend API (port 3010). Runs DB migrations + creates the +# limited app role on first boot. /api/health is the health endpoint. +# ---------------------------------------------------------------------------- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: sparkyfitness-server + namespace: workout-system + labels: + app.kubernetes.io/instance: sparkyfitness + app.kubernetes.io/name: server +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + app.kubernetes.io/instance: sparkyfitness + app.kubernetes.io/name: server + template: + metadata: + labels: + app.kubernetes.io/instance: sparkyfitness + app.kubernetes.io/name: server + annotations: + # Tag format is vMAJOR.MINOR.PATCH.BUILD (e.g. v0.16.6.3) + match-regex.version-checker.io/server: '^v\d+\.\d+\.\d+\.\d+$' + spec: + enableServiceLinks: false + securityContext: + # node user (UID 1000) needs to write to the uploads PVC + fsGroup: 1000 + initContainers: + - name: wait-for-db + image: busybox:1.36 + command: + - sh + - -c + - | + echo "Waiting for PostgreSQL..." + until nc -z sparkyfitness-db 5432; do + echo "PostgreSQL not ready, waiting..." + sleep 2 + done + echo "PostgreSQL is ready!" + containers: + - name: server + image: codewithcj/sparkyfitness_server:v0.16.6.3 + env: + # ---- Database (owner / superuser role, used for migrations) ---- + - name: SPARKY_FITNESS_DB_HOST + value: "sparkyfitness-db" + - name: SPARKY_FITNESS_DB_PORT + value: "5432" + - name: SPARKY_FITNESS_DB_NAME + value: "sparkyfitness_db" + - name: SPARKY_FITNESS_DB_USER + valueFrom: + secretKeyRef: + name: sparky-app + key: db-user + - name: SPARKY_FITNESS_DB_PASSWORD + valueFrom: + secretKeyRef: + name: sparky-app + key: db-password + # ---- Limited-privilege app role (auto-created on first boot) ---- + - name: SPARKY_FITNESS_APP_DB_USER + valueFrom: + secretKeyRef: + name: sparky-app + key: app-db-user + - name: SPARKY_FITNESS_APP_DB_PASSWORD + valueFrom: + secretKeyRef: + name: sparky-app + key: app-db-password + # ---- Crypto secrets (SET ONCE, NEVER CHANGE) ---- + - name: SPARKY_FITNESS_API_ENCRYPTION_KEY + valueFrom: + secretKeyRef: + name: sparky-app + key: api-encryption-key + - name: BETTER_AUTH_SECRET + valueFrom: + secretKeyRef: + name: sparky-app + key: better-auth-secret + # ---- App config ---- + - name: NODE_ENV + value: "production" + - name: TZ + value: "Europe/Budapest" + - name: SPARKY_FITNESS_LOG_LEVEL + value: "INFO" + - name: SPARKY_FITNESS_FRONTEND_URL + value: "https://workout.dooplex.hu" + - name: SPARKY_FITNESS_DISABLE_SIGNUP + value: "true" + - name: SPARKY_FITNESS_ADMIN_EMAIL + value: "nagyfenyvesi.viktor@gmail.com" + - name: ALLOW_PRIVATE_NETWORK_CORS + value: "true" + - name: SPARKY_FITNESS_EXTRA_TRUSTED_ORIGINS + value: "https://workout.home" + # ---- Fail-safe: keep email/password login working during bring-up + # so a misconfigured OIDC can't lock you out. REMOVE this (and + # optionally set SPARKY_FITNESS_DISABLE_EMAIL_LOGIN=true) once the + # Authentik OIDC login is confirmed working end-to-end. + - name: SPARKY_FITNESS_FORCE_EMAIL_LOGIN + value: "true" + # ---- OIDC (Authentik) — env-based provider upsert ---- + - name: SPARKY_FITNESS_OIDC_AUTH_ENABLED + value: "true" + - name: SPARKY_FITNESS_OIDC_ISSUER_URL + value: "https://authentik.dooplex.hu/application/o/sparkyfitness/" + - name: SPARKY_FITNESS_OIDC_CLIENT_ID + valueFrom: + secretKeyRef: + name: sparky-oauth + key: client-id + - name: SPARKY_FITNESS_OIDC_CLIENT_SECRET + valueFrom: + secretKeyRef: + name: sparky-oauth + key: client-secret + - name: SPARKY_FITNESS_OIDC_PROVIDER_SLUG + value: "authentik" + - name: SPARKY_FITNESS_OIDC_PROVIDER_NAME + value: "Authentik" + - name: SPARKY_FITNESS_OIDC_SCOPE + value: "openid profile email" + - name: SPARKY_FITNESS_OIDC_AUTO_REGISTER + value: "true" + ports: + - containerPort: 3010 + name: http + resources: + requests: + cpu: 200m + memory: 256Mi + limits: + cpu: 1000m + memory: 1Gi + volumeMounts: + - name: uploads + mountPath: /app/SparkyFitnessServer/uploads + # First boot runs DB migrations; startupProbe gives it room before + # liveness/readiness kick in. + startupProbe: + httpGet: + path: /api/health + port: http + periodSeconds: 10 + failureThreshold: 30 + livenessProbe: + httpGet: + path: /api/health + port: http + periodSeconds: 10 + timeoutSeconds: 10 + failureThreshold: 5 + readinessProbe: + httpGet: + path: /api/health + port: http + periodSeconds: 5 + timeoutSeconds: 10 + volumes: + - name: uploads + persistentVolumeClaim: + claimName: sparkyfitness-uploads +--- +# ---------------------------------------------------------------------------- +# Frontend: nginx serving the SPA. Its internal nginx proxies /api, /uploads, +# etc. to the server, so the ingress only needs to target this one Service. +# Root image listens on :80 (listen 80; hardcoded) — NGINX_LISTEN_PORT pinned. +# ---------------------------------------------------------------------------- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: sparkyfitness-frontend + namespace: workout-system + labels: + app.kubernetes.io/instance: sparkyfitness + app.kubernetes.io/name: frontend +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + app.kubernetes.io/instance: sparkyfitness + app.kubernetes.io/name: frontend + template: + metadata: + labels: + app.kubernetes.io/instance: sparkyfitness + app.kubernetes.io/name: frontend + annotations: + match-regex.version-checker.io/frontend: '^v\d+\.\d+\.\d+\.\d+$' + spec: + enableServiceLinks: false + containers: + - name: frontend + image: codewithcj/sparkyfitness:v0.16.6.3 + env: + - name: SPARKY_FITNESS_SERVER_HOST + value: "sparkyfitness-server" + - name: SPARKY_FITNESS_SERVER_PORT + value: "3010" + - name: SPARKY_FITNESS_FRONTEND_URL + value: "https://workout.dooplex.hu" + - name: NGINX_LISTEN_PORT + value: "80" + - name: TZ + value: "Europe/Budapest" + ports: + - containerPort: 80 + name: http + resources: + requests: + cpu: 50m + memory: 64Mi + limits: + cpu: 500m + memory: 256Mi + livenessProbe: + httpGet: + path: / + port: http + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 10 + failureThreshold: 5 + readinessProbe: + httpGet: + path: / + port: http + initialDelaySeconds: 5 + periodSeconds: 5 + timeoutSeconds: 10 +--- +apiVersion: v1 +kind: Service +metadata: + name: sparkyfitness-db + namespace: workout-system + labels: + app.kubernetes.io/instance: sparkyfitness + app.kubernetes.io/name: postgres +spec: + type: ClusterIP + ports: + - name: postgres + port: 5432 + targetPort: postgres + selector: + app.kubernetes.io/instance: sparkyfitness + app.kubernetes.io/name: postgres +--- +apiVersion: v1 +kind: Service +metadata: + name: sparkyfitness-server + namespace: workout-system + labels: + app.kubernetes.io/instance: sparkyfitness + app.kubernetes.io/name: server +spec: + type: ClusterIP + ports: + - name: http + port: 3010 + targetPort: http + selector: + app.kubernetes.io/instance: sparkyfitness + app.kubernetes.io/name: server +--- +apiVersion: v1 +kind: Service +metadata: + name: sparkyfitness-frontend + namespace: workout-system + labels: + app.kubernetes.io/instance: sparkyfitness + app.kubernetes.io/name: frontend +spec: + type: ClusterIP + ports: + - name: http + port: 80 + targetPort: http + selector: + app.kubernetes.io/instance: sparkyfitness + app.kubernetes.io/name: frontend +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: sparkyfitness-postgres + namespace: workout-system + labels: + app.kubernetes.io/instance: sparkyfitness + app.kubernetes.io/name: postgres + recurring-job-group.longhorn.io/needbackup: enabled + recurring-job.longhorn.io/source: enabled +spec: + accessModes: + - ReadWriteOnce + storageClassName: longhorn + resources: + requests: + storage: 5Gi +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: sparkyfitness-uploads + namespace: workout-system + labels: + app.kubernetes.io/instance: sparkyfitness + app.kubernetes.io/name: server + recurring-job-group.longhorn.io/needbackup: enabled + recurring-job.longhorn.io/source: enabled +spec: + accessModes: + - ReadWriteOnce + storageClassName: longhorn + resources: + requests: + storage: 5Gi +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: sparkyfitness + namespace: workout-system + labels: + app.kubernetes.io/instance: sparkyfitness + app.kubernetes.io/name: frontend + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod + external-dns.alpha.kubernetes.io/hostname: workout.dooplex.hu,workout.home + nginx.ingress.kubernetes.io/ssl-redirect: "true" + nginx.ingress.kubernetes.io/proxy-body-size: "50m" + # Auth is handled IN-APP (native OIDC) — no forward-auth annotations here. + # Hungary-only geo-block (same pattern as the rest of the DooPlex apps). + nginx.ingress.kubernetes.io/configuration-snippet: | + set $geo_allowed 0; + if ($remote_addr ~ "^192\.168\.") { set $geo_allowed 1; } + if ($remote_addr ~ "^10\.") { set $geo_allowed 1; } + if ($geoip2_country_code = "HU") { set $geo_allowed 1; } + if ($geo_allowed = 0) { + return 403 "Access restricted to Hungary"; + } +spec: + ingressClassName: nginx-internal + rules: + - host: workout.dooplex.hu + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: sparkyfitness-frontend + port: + number: 80 + - host: workout.home + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: sparkyfitness-frontend + port: + number: 80 + tls: + - hosts: + - workout.dooplex.hu + secretName: sparkyfitness-tls diff --git a/workout-system/workout.yaml b/workout-system/workout.yaml index f99b906..a097939 100644 --- a/workout-system/workout.yaml +++ b/workout-system/workout.yaml @@ -1,3 +1,13 @@ +# ============================================================================ +# *** PARKED 2026-05-27 *** — wger has been REPLACED by SparkyFitness. +# SparkyFitness now owns workout.dooplex.hu / workout.home (see sparkyfitness.yaml). +# All wger Deployments are scaled to 0 and both wger Ingresses were removed +# (ArgoCD prune deletes them, freeing the hostnames). The wger Services, +# ConfigMap, PVCs (wger-media / wger-static) and the wger DB in the shared CNPG +# cluster are KEPT, untouched, for rollback. +# To revive wger: restore the two Ingress resources from git history and scale +# the wger / wger-redis / wger-celery-* Deployments back to 1. +# ============================================================================ # wger - Workout Manager # https://github.com/wger-project/wger # Version: 2.5 (official image, no custom fork) @@ -40,7 +50,7 @@ metadata: app.kubernetes.io/instance: wger app.kubernetes.io/name: wger-redis spec: - replicas: 1 + replicas: 0 # parked 2026-05-27 (replaced by SparkyFitness) selector: matchLabels: app.kubernetes.io/instance: wger @@ -78,7 +88,7 @@ metadata: extensions.v1alpha1.version-checker.io/wger: "true" extensions.v1alpha1.version-checker.io/wger.match-regex: "^\\d+\\.\\d+$" spec: - replicas: 1 + replicas: 0 # parked 2026-05-27 (replaced by SparkyFitness) selector: matchLabels: app.kubernetes.io/instance: wger @@ -256,7 +266,7 @@ metadata: app.kubernetes.io/instance: wger app.kubernetes.io/name: wger-celery-worker spec: - replicas: 1 + replicas: 0 # parked 2026-05-27 (replaced by SparkyFitness) selector: matchLabels: app.kubernetes.io/instance: wger @@ -336,7 +346,7 @@ metadata: app.kubernetes.io/instance: wger app.kubernetes.io/name: wger-celery-beat spec: - replicas: 1 + replicas: 0 # parked 2026-05-27 (replaced by SparkyFitness) selector: matchLabels: app.kubernetes.io/instance: wger @@ -439,117 +449,12 @@ spec: app.kubernetes.io/name: wger --- # ============================================================================ -# Ingress #1: web UI paths (/) - Authentik forward-auth protected +# wger Ingresses (wger + wger-api) REMOVED 2026-05-27 — see PARKED note at top. +# SparkyFitness's ingress (sparkyfitness.yaml) now serves workout.dooplex.hu / +# workout.home. ArgoCD prune deletes the old Ingress objects from the cluster, +# releasing the hostnames + the wger-tls certificate's hosts. +# To revive wger: restore these two Ingress resources from git history. # ============================================================================ -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: wger - namespace: workout-system - labels: - app.kubernetes.io/instance: wger - app.kubernetes.io/name: wger - annotations: - cert-manager.io/cluster-issuer: letsencrypt-prod - external-dns.alpha.kubernetes.io/hostname: workout.dooplex.hu,workout.home - nginx.ingress.kubernetes.io/ssl-redirect: "true" - nginx.ingress.kubernetes.io/proxy-body-size: "100m" - # Authentik Forward Auth (domain mode) - same pattern as your other SSO apps - # If you use an internal outpost service URL elsewhere, swap auth-url for it. - nginx.ingress.kubernetes.io/auth-url: http://ak-outpost-kisfenyo-outpost.auth-system.svc.cluster.local:9000/outpost.goauthentik.io/auth/nginx - nginx.ingress.kubernetes.io/auth-signin: https://workout.dooplex.hu/outpost.goauthentik.io/start?rd=$escaped_request_uri - nginx.ingress.kubernetes.io/auth-response-headers: "Set-Cookie,X-Authentik-Username,X-Authentik-Email,X-Authentik-Name,X-Authentik-Groups,X-Authentik-Uid" - nginx.ingress.kubernetes.io/auth-snippet: | - proxy_set_header X-Forwarded-Host $http_host; - nginx.ingress.kubernetes.io/configuration-snippet: | - set $geo_allowed 0; - if ($remote_addr ~ "^192\.168\.") { set $geo_allowed 1; } - if ($remote_addr ~ "^10\.") { set $geo_allowed 1; } - if ($geoip2_country_code = "HU") { set $geo_allowed 1; } - if ($geo_allowed = 0) { - return 403 "Access restricted to Hungary"; - } -spec: - ingressClassName: nginx-internal - rules: - - host: workout.dooplex.hu - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: wger - port: - number: 80 - - host: workout.home - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: wger - port: - number: 80 - tls: - - hosts: - - workout.dooplex.hu - secretName: wger-tls ---- -# ============================================================================ -# Ingress #2: API paths (/api/) - NO forward-auth, JWT token auth only -# Required so the wger Flutter mobile app can hit /api/v2/token for login. -# More-specific path match means /api/* hits this Ingress, not the / one. -# ============================================================================ -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: wger-api - namespace: workout-system - labels: - app.kubernetes.io/instance: wger - app.kubernetes.io/name: wger-api - annotations: - cert-manager.io/cluster-issuer: letsencrypt-prod - nginx.ingress.kubernetes.io/ssl-redirect: "true" - nginx.ingress.kubernetes.io/proxy-body-size: "100m" - nginx.ingress.kubernetes.io/configuration-snippet: | - # Same geo-block as the web UI ingress - set $geo_allowed 0; - if ($remote_addr ~ "^192\.168\.") { set $geo_allowed 1; } - if ($remote_addr ~ "^10\.") { set $geo_allowed 1; } - if ($geoip2_country_code = "HU") { set $geo_allowed 1; } - if ($geo_allowed = 0) { - return 403 "Access restricted to Hungary"; - } -spec: - ingressClassName: nginx-internal - rules: - - host: workout.dooplex.hu - http: - paths: - - path: /api - pathType: Prefix - backend: - service: - name: wger - port: - number: 80 - - host: workout.home - http: - paths: - - path: /api - pathType: Prefix - backend: - service: - name: wger - port: - number: 80 - tls: - - hosts: - - workout.dooplex.hu - secretName: wger-tls --- apiVersion: v1 kind: PersistentVolumeClaim