diff --git a/.gitea/workflows/docker-build.yml b/.gitea/workflows/docker-build.yml index d12b817..5f2c601 100644 --- a/.gitea/workflows/docker-build.yml +++ b/.gitea/workflows/docker-build.yml @@ -29,11 +29,11 @@ jobs: uses: actions/checkout@v4 - name: Image-Namen ermitteln - # Fester Produktname "artista-filmcamp" unabhaengig vom Repo-Namen; + # Fester Produktname "artista-modelcamp" unabhaengig vom Repo-Namen; # nur der Besitzer kommt aus Gitea, in Kleinbuchstaben (OCI-Vorgabe): - # "Nick.Rosenloecher" -> "nick.rosenloecher/artista-filmcamp". + # "Nick.Rosenloecher" -> "nick.rosenloecher/artista-modelcamp". id: image - run: echo "name=$(echo '${{ gitea.repository_owner }}' | tr '[:upper:]' '[:lower:]')/artista-filmcamp" >> "$GITEA_OUTPUT" + run: echo "name=$(echo '${{ gitea.repository_owner }}' | tr '[:upper:]' '[:lower:]')/artista-modelcamp" >> "$GITEA_OUTPUT" - name: Docker Buildx einrichten uses: docker/setup-buildx-action@v3 diff --git a/Dockerfile b/Dockerfile index c548c29..f970096 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,8 @@ # syntax=docker/dockerfile:1 # # Zwei Images aus einem Build: -# docker build --target runner -t /artista-filmcamp: . -# docker build --target tools -t /artista-filmcamp-tools: . +# docker build --target runner -t /artista-modelcamp: . +# docker build --target tools -t /artista-modelcamp-tools: . # # runner: der Next.js-Server (standalone), sonst nichts. # tools: Prisma-CLI + tsx fuer den kontrollierten Migrationsschritt diff --git a/docs/BETRIEB.md b/docs/BETRIEB.md index 65c391d..577d9c5 100644 --- a/docs/BETRIEB.md +++ b/docs/BETRIEB.md @@ -16,8 +16,8 @@ Ein Release entsteht dann so: `git tag v0.1.0 && git push origin v0.1.0`, anschl **Manuell:** ```bash -docker build --target runner -t git.encry.de/nick.rosenloecher/artista-filmcamp:0.1.0 . -docker build --target tools -t git.encry.de/nick.rosenloecher/artista-filmcamp-tools:0.1.0 . +docker build --target runner -t git.encry.de/nick.rosenloecher/artista-modelcamp:0.1.0 . +docker build --target tools -t git.encry.de/nick.rosenloecher/artista-modelcamp-tools:0.1.0 . docker push … # beide Images, immer mit identischem Tag ``` @@ -35,8 +35,8 @@ Der Build braucht Internet für `next/font` (Schriften werden eingebettet), aber 5. **Migration** (siehe Abschnitt 3), danach das erste Admin-Konto: ```bash kubectl -n artista run admin-create --rm -it --restart=Never --labels=artista/db-client=true \ - --image=git.encry.de/nick.rosenloecher/artista-filmcamp-tools:0.1.0 \ - --overrides='{"spec":{"containers":[{"name":"admin-create","image":"git.encry.de/nick.rosenloecher/artista-filmcamp-tools:0.1.0","command":["tsx","scripts/create-admin.ts","--email","du@example.org","--name","Dein Name"],"envFrom":[{"configMapRef":{"name":"artista-filmcamp-config"}},{"secretRef":{"name":"artista-filmcamp-secrets"}}]}]}}' + --image=git.encry.de/nick.rosenloecher/artista-modelcamp-tools:0.1.0 \ + --overrides='{"spec":{"containers":[{"name":"admin-create","image":"git.encry.de/nick.rosenloecher/artista-modelcamp-tools:0.1.0","command":["tsx","scripts/create-admin.ts","--email","du@example.org","--name","Dein Name"],"envFrom":[{"configMapRef":{"name":"artista-modelcamp-config"}},{"secretRef":{"name":"artista-modelcamp-secrets"}}]}]}}' ``` Das einmalige Passwort wird ausgegeben. Bei der ersten Anmeldung unter `/admin` wird die 2FA eingerichtet. @@ -47,10 +47,10 @@ Der Build braucht Internet für `next/font` (Schriften werden eingebettet), aber kubectl apply -k k8s/overlays/production # 2. Migration bewusst starten und abwarten JOB=migrate-$(date +%Y%m%d%H%M) -kubectl -n artista create job $JOB --from=cronjob/artista-filmcamp-migrate +kubectl -n artista create job $JOB --from=cronjob/artista-modelcamp-migrate kubectl -n artista wait --for=condition=complete job/$JOB --timeout=5m # 3. Rollout beobachten -kubectl -n artista rollout status deploy/artista-filmcamp-app +kubectl -n artista rollout status deploy/artista-modelcamp-app ``` Migrationen sind vorwärtskompatibel zu schreiben (erst Spalten ergänzen, später entfernen), damit alte und neue Pods während des Rollouts parallel laufen können. @@ -71,7 +71,7 @@ Migrationen sind vorwärtskompatibel zu schreiben (erst Spalten ergänzen, spät ## 6. Worker -Der CronJob `artista-filmcamp-worker` läuft alle 10 Minuten und kümmert sich um: +Der CronJob `artista-modelcamp-worker` läuft alle 10 Minuten und kümmert sich um: - abgelaufene Reservierungen, vorher mit Rückfrage bei Stripe - hängende Einzüge - fällige Raten @@ -83,18 +83,18 @@ Ein fehlgeschlagener Lauf erscheint als fehlgeschlagener Job (`kubectl -n artist ## 7. Sicherung und Wiederherstellung -- **Sicherung:** Der CronJob `artista-filmcamp-db-backup` erstellt täglich um 3:15 Uhr einen Dump im `pg_dump`-Custom-Format nach `artista-filmcamp-db-backup` und bewahrt ihn 30 Tage auf. +- **Sicherung:** Der CronJob `artista-modelcamp-db-backup` erstellt täglich um 3:15 Uhr einen Dump im `pg_dump`-Custom-Format nach `artista-modelcamp-db-backup` und bewahrt ihn 30 Tage auf. - **Aus dem Cluster sichern (regelmäßig):** ```bash - POD=$(kubectl -n artista get pod -l app.kubernetes.io/name=artista-filmcamp-postgres -o name | head -1) + POD=$(kubectl -n artista get pod -l app.kubernetes.io/name=artista-modelcamp-postgres -o name | head -1) kubectl -n artista exec $POD -- pg_dump -U artista -d artista --format=custom > artista-$(date +%F).dump ``` - **Wiederherstellen:** - 1. App stoppen: `kubectl -n artista scale deploy/artista-filmcamp-app --replicas=0` und den Worker-CronJob pausieren: `kubectl -n artista patch cronjob artista-filmcamp-worker -p '{"spec":{"suspend":true}}'` + 1. App stoppen: `kubectl -n artista scale deploy/artista-modelcamp-app --replicas=0` und den Worker-CronJob pausieren: `kubectl -n artista patch cronjob artista-modelcamp-worker -p '{"spec":{"suspend":true}}'` 2. Einspielen: `kubectl -n artista exec -i $POD -- pg_restore -U artista -d artista --clean --if-exists < artista-YYYY-MM-DD.dump` 3. Migration laufen lassen (Abschnitt 3), App wieder hochskalieren, Worker fortsetzen. 4. Abgleich: Zahlungen, die nach dem Sicherungszeitpunkt eingingen, im Stripe-Dashboard prüfen. Stripe stellt Webhooks bis zu drei Tage erneut zu (Dashboard → Webhooks → „Erneut senden“). -- **Medien:** Das Volume `artista-filmcamp-media` separat sichern, z. B. mit Volume-Snapshots oder der Sicherung des Speichersystems. +- **Medien:** Das Volume `artista-modelcamp-media` separat sichern, z. B. mit Volume-Snapshots oder der Sicherung des Speichersystems. - **Wiederherstellung üben:** mindestens einmal vor dem Live-Verkauf in einen Test-Namespace. ## 8. Stornierung und Erstattung @@ -108,7 +108,7 @@ Die Software trifft keine Storno-Entscheidung, sie setzt sie nur nachvollziehbar ## 9. Medien -Dateien (jpg, png, webp, avif, mp4, webm) in das Volume `artista-filmcamp-media` kopieren: +Dateien (jpg, png, webp, avif, mp4, webm) in das Volume `artista-modelcamp-media` kopieren: ```bash kubectl -n artista cp villa-aussen.jpg :/app/media/villa-aussen.jpg # Volume ist im App-Pod read-only → einen Hilfs-Pod mit Schreibzugriff nutzen diff --git a/k8s/base/app.yaml b/k8s/base/app.yaml index 2297d1b..0ff7a92 100644 --- a/k8s/base/app.yaml +++ b/k8s/base/app.yaml @@ -3,11 +3,11 @@ # PostgreSQL — beliebig viele Replikate moeglich. # # Migrationen laufen NICHT beim Start, sondern als eigener, kontrollierter -# Schritt (jobs.yaml, CronJob "artista-filmcamp-migrate", siehe docs/BETRIEB.md). +# Schritt (jobs.yaml, CronJob "artista-modelcamp-migrate", siehe docs/BETRIEB.md). apiVersion: apps/v1 kind: Deployment metadata: - name: artista-filmcamp-app + name: artista-modelcamp-app spec: replicas: 2 revisionHistoryLimit: 5 @@ -16,11 +16,11 @@ spec: rollingUpdate: { maxSurge: 1, maxUnavailable: 0 } selector: matchLabels: - app.kubernetes.io/name: artista-filmcamp-app + app.kubernetes.io/name: artista-modelcamp-app template: metadata: labels: - app.kubernetes.io/name: artista-filmcamp-app + app.kubernetes.io/name: artista-modelcamp-app artista/db-client: "true" spec: automountServiceAccountToken: false @@ -36,16 +36,16 @@ spec: whenUnsatisfiable: ScheduleAnyway labelSelector: matchLabels: - app.kubernetes.io/name: artista-filmcamp-app + app.kubernetes.io/name: artista-modelcamp-app containers: - name: app - image: artista-filmcamp + image: artista-modelcamp ports: - name: http containerPort: 3000 envFrom: - - configMapRef: { name: artista-filmcamp-config } - - secretRef: { name: artista-filmcamp-secrets } + - configMapRef: { name: artista-modelcamp-config } + - secretRef: { name: artista-modelcamp-secrets } readinessProbe: httpGet: { path: /api/health/ready, port: http } periodSeconds: 10 @@ -76,7 +76,7 @@ spec: mountPath: /tmp volumes: - name: media - persistentVolumeClaim: { claimName: artista-filmcamp-media } + persistentVolumeClaim: { claimName: artista-modelcamp-media } - name: next-cache emptyDir: { sizeLimit: 512Mi } - name: tmp @@ -85,10 +85,10 @@ spec: apiVersion: v1 kind: Service metadata: - name: artista-filmcamp-app + name: artista-modelcamp-app spec: selector: - app.kubernetes.io/name: artista-filmcamp-app + app.kubernetes.io/name: artista-modelcamp-app ports: - name: http port: 80 @@ -102,11 +102,11 @@ spec: apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: - name: artista-filmcamp-app-ingress + name: artista-modelcamp-app-ingress spec: podSelector: matchLabels: - app.kubernetes.io/name: artista-filmcamp-app + app.kubernetes.io/name: artista-modelcamp-app policyTypes: ["Ingress"] ingress: - from: @@ -121,9 +121,9 @@ spec: apiVersion: policy/v1 kind: PodDisruptionBudget metadata: - name: artista-filmcamp-app + name: artista-modelcamp-app spec: minAvailable: 1 selector: matchLabels: - app.kubernetes.io/name: artista-filmcamp-app + app.kubernetes.io/name: artista-modelcamp-app diff --git a/k8s/base/config.yaml b/k8s/base/config.yaml index ff1a7e3..be5c1be 100644 --- a/k8s/base/config.yaml +++ b/k8s/base/config.yaml @@ -10,7 +10,7 @@ metadata: apiVersion: v1 kind: ConfigMap metadata: - name: artista-filmcamp-config + name: artista-modelcamp-config data: APP_ENV: "production" APP_URL: "https://modelcamp.example.org" @@ -33,7 +33,7 @@ data: apiVersion: v1 kind: PersistentVolumeClaim metadata: - name: artista-filmcamp-media + name: artista-modelcamp-media spec: accessModes: ["ReadWriteMany"] # storageClassName: longhorn @@ -46,11 +46,11 @@ spec: apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: - name: artista-filmcamp-postgres-access + name: artista-modelcamp-postgres-access spec: podSelector: matchLabels: - app.kubernetes.io/name: artista-filmcamp-postgres + app.kubernetes.io/name: artista-modelcamp-postgres policyTypes: ["Ingress"] ingress: - from: diff --git a/k8s/base/jobs.yaml b/k8s/base/jobs.yaml index 9147e52..a85248e 100644 --- a/k8s/base/jobs.yaml +++ b/k8s/base/jobs.yaml @@ -2,12 +2,12 @@ # Als SUSPENDIERTER CronJob definiert, damit Kustomize das Image mit der # App gleichzieht. Er laeuft nie von selbst, sondern bewusst vor jedem # Rollout: -# kubectl -n artista create job migrate-$(date +%Y%m%d%H%M) --from=cronjob/artista-filmcamp-migrate +# kubectl -n artista create job migrate-$(date +%Y%m%d%H%M) --from=cronjob/artista-modelcamp-migrate # kubectl -n artista wait --for=condition=complete job/migrate-… --timeout=5m apiVersion: batch/v1 kind: CronJob metadata: - name: artista-filmcamp-migrate + name: artista-modelcamp-migrate spec: suspend: true schedule: "0 0 1 1 *" @@ -28,10 +28,10 @@ spec: seccompProfile: { type: RuntimeDefault } containers: - name: migrate - image: artista-filmcamp-tools + image: artista-modelcamp-tools command: ["prisma", "migrate", "deploy"] envFrom: - - secretRef: { name: artista-filmcamp-secrets } + - secretRef: { name: artista-modelcamp-secrets } resources: requests: { cpu: 50m, memory: 128Mi } limits: { memory: 512Mi } @@ -47,7 +47,7 @@ spec: apiVersion: batch/v1 kind: CronJob metadata: - name: artista-filmcamp-worker + name: artista-modelcamp-worker spec: schedule: "*/10 * * * *" timeZone: "Europe/Berlin" @@ -72,11 +72,11 @@ spec: seccompProfile: { type: RuntimeDefault } containers: - name: worker - image: artista-filmcamp-tools + image: artista-modelcamp-tools command: ["tsx", "scripts/worker.ts"] envFrom: - - configMapRef: { name: artista-filmcamp-config } - - secretRef: { name: artista-filmcamp-secrets } + - configMapRef: { name: artista-modelcamp-config } + - secretRef: { name: artista-modelcamp-secrets } resources: requests: { cpu: 50m, memory: 192Mi } limits: { memory: 512Mi } @@ -91,7 +91,7 @@ spec: apiVersion: v1 kind: PersistentVolumeClaim metadata: - name: artista-filmcamp-db-backup + name: artista-modelcamp-db-backup spec: accessModes: ["ReadWriteOnce"] # storageClassName: smb-backup @@ -102,7 +102,7 @@ spec: apiVersion: batch/v1 kind: CronJob metadata: - name: artista-filmcamp-db-backup + name: artista-modelcamp-db-backup spec: schedule: "15 3 * * *" timeZone: "Europe/Berlin" @@ -131,7 +131,7 @@ spec: env: - name: PGPASSWORD valueFrom: - secretKeyRef: { name: artista-filmcamp-secrets, key: DB_PASSWORD } + secretKeyRef: { name: artista-modelcamp-secrets, key: DB_PASSWORD } command: ["/bin/sh", "-c"] args: - | @@ -139,7 +139,7 @@ spec: set -o pipefail target="/backup/artista-$(date -u +%Y-%m-%d_%H%M).dump" # Custom-Format: komprimiert, selektiv wiederherstellbar. - pg_dump --host=artista-filmcamp-postgres --username=artista --dbname=artista \ + pg_dump --host=artista-modelcamp-postgres --username=artista --dbname=artista \ --format=custom --file="${target}.partial" mv "${target}.partial" "${target}" find /backup -name 'artista-*.dump.partial' -mtime +1 -delete @@ -156,4 +156,4 @@ spec: mountPath: /backup volumes: - name: backup - persistentVolumeClaim: { claimName: artista-filmcamp-db-backup } + persistentVolumeClaim: { claimName: artista-modelcamp-db-backup } diff --git a/k8s/base/kustomization.yaml b/k8s/base/kustomization.yaml index a1e3195..78569a7 100644 --- a/k8s/base/kustomization.yaml +++ b/k8s/base/kustomization.yaml @@ -10,5 +10,5 @@ resources: - jobs.yaml labels: - pairs: - app.kubernetes.io/part-of: artista-filmcamp + app.kubernetes.io/part-of: artista-modelcamp includeSelectors: false diff --git a/k8s/base/postgres.yaml b/k8s/base/postgres.yaml index 2b1a131..7798c41 100644 --- a/k8s/base/postgres.yaml +++ b/k8s/base/postgres.yaml @@ -12,17 +12,17 @@ apiVersion: apps/v1 kind: StatefulSet metadata: - name: artista-filmcamp-postgres + name: artista-modelcamp-postgres spec: - serviceName: artista-filmcamp-postgres + serviceName: artista-modelcamp-postgres replicas: 1 selector: matchLabels: - app.kubernetes.io/name: artista-filmcamp-postgres + app.kubernetes.io/name: artista-modelcamp-postgres template: metadata: labels: - app.kubernetes.io/name: artista-filmcamp-postgres + app.kubernetes.io/name: artista-modelcamp-postgres spec: securityContext: runAsNonRoot: true @@ -42,7 +42,7 @@ spec: value: artista - name: POSTGRES_PASSWORD valueFrom: - secretKeyRef: { name: artista-filmcamp-secrets, key: DB_PASSWORD } + secretKeyRef: { name: artista-modelcamp-secrets, key: DB_PASSWORD } volumeMounts: # Ab PostgreSQL 18 liegt PGDATA unter /var/lib/postgresql/18/docker. - name: data @@ -73,10 +73,10 @@ spec: apiVersion: v1 kind: Service metadata: - name: artista-filmcamp-postgres + name: artista-modelcamp-postgres spec: clusterIP: None selector: - app.kubernetes.io/name: artista-filmcamp-postgres + app.kubernetes.io/name: artista-modelcamp-postgres ports: - port: 5432 diff --git a/k8s/overlays/production/ingress.yaml b/k8s/overlays/production/ingress.yaml index 7caa447..4722eb5 100644 --- a/k8s/overlays/production/ingress.yaml +++ b/k8s/overlays/production/ingress.yaml @@ -8,7 +8,7 @@ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: - name: artista-filmcamp + name: artista-modelcamp annotations: cert-manager.io/cluster-issuer: letsencrypt-prod traefik.ingress.kubernetes.io/router.entrypoints: websecure @@ -18,7 +18,7 @@ spec: tls: - hosts: - modelcamp.example.org - secretName: artista-filmcamp-tls + secretName: artista-modelcamp-tls rules: - host: modelcamp.example.org http: @@ -27,7 +27,7 @@ spec: pathType: Prefix backend: service: - name: artista-filmcamp-app + name: artista-modelcamp-app port: name: http --- @@ -36,10 +36,10 @@ spec: apiVersion: networking.k8s.io/v1 kind: Ingress metadata: - name: artista-filmcamp-http-redirect + name: artista-modelcamp-http-redirect annotations: traefik.ingress.kubernetes.io/router.entrypoints: web - traefik.ingress.kubernetes.io/router.middlewares: artista-artista-filmcamp-redirect-https@kubernetescrd + traefik.ingress.kubernetes.io/router.middlewares: artista-artista-modelcamp-redirect-https@kubernetescrd spec: ingressClassName: traefik rules: @@ -50,14 +50,14 @@ spec: pathType: Prefix backend: service: - name: artista-filmcamp-app + name: artista-modelcamp-app port: name: http --- apiVersion: traefik.io/v1alpha1 kind: Middleware metadata: - name: artista-filmcamp-redirect-https + name: artista-modelcamp-redirect-https spec: redirectScheme: scheme: https diff --git a/k8s/overlays/production/kustomization.yaml b/k8s/overlays/production/kustomization.yaml index 9ea2373..8249efd 100644 --- a/k8s/overlays/production/kustomization.yaml +++ b/k8s/overlays/production/kustomization.yaml @@ -12,15 +12,15 @@ images: # Gebaut von .gitea/workflows/docker-build.yml: Git-Tag "v0.1.0" pushen # ergibt Image-Tag "0.1.0" (Alternativen: "sha-", "latest"). # Beide Images immer mit identischem Tag ausrollen. - - name: artista-filmcamp - newName: git.encry.de/nick.rosenloecher/artista-filmcamp + - name: artista-modelcamp + newName: git.encry.de/nick.rosenloecher/artista-modelcamp newTag: "0.1.0" - - name: artista-filmcamp-tools - newName: git.encry.de/nick.rosenloecher/artista-filmcamp-tools + - name: artista-modelcamp-tools + newName: git.encry.de/nick.rosenloecher/artista-modelcamp-tools newTag: "0.1.0" patches: - - target: { kind: ConfigMap, name: artista-filmcamp-config } + - target: { kind: ConfigMap, name: artista-modelcamp-config } patch: |- - op: replace path: /data/APP_URL diff --git a/k8s/secret.example.yaml b/k8s/secret.example.yaml index 6dcd9c3..9e24004 100644 --- a/k8s/secret.example.yaml +++ b/k8s/secret.example.yaml @@ -1,8 +1,8 @@ # VORLAGE — nicht mit echten Werten ins Repository einchecken. # Anlegen z. B. per: -# kubectl -n artista create secret generic artista-filmcamp-secrets \ +# kubectl -n artista create secret generic artista-modelcamp-secrets \ # --from-literal=DB_PASSWORD='…' \ -# --from-literal=DATABASE_URL='postgresql://artista:…@artista-filmcamp-postgres:5432/artista' \ +# --from-literal=DATABASE_URL='postgresql://artista:…@artista-modelcamp-postgres:5432/artista' \ # --from-literal=APP_SECRET="$(openssl rand -base64 48)" \ # --from-literal=BETTER_AUTH_SECRET="$(openssl rand -base64 48)" \ # --from-literal=STRIPE_SECRET_KEY='sk_test_…' \ @@ -12,13 +12,13 @@ apiVersion: v1 kind: Secret metadata: - name: artista-filmcamp-secrets + name: artista-modelcamp-secrets namespace: artista type: Opaque stringData: DB_PASSWORD: "" # Passwort URL-kodiert einsetzen, falls Sonderzeichen enthalten sind. - DATABASE_URL: "postgresql://artista:@artista-filmcamp-postgres:5432/artista" + DATABASE_URL: "postgresql://artista:@artista-modelcamp-postgres:5432/artista" APP_SECRET: "" BETTER_AUTH_SECRET: "" STRIPE_SECRET_KEY: ""