From 55c04e4fd03a88abed3fc5e58bd2e09defa77b70 Mon Sep 17 00:00:00 2001 From: Michael Schiemer Date: Fri, 31 Oct 2025 01:31:44 +0100 Subject: [PATCH] ci: setup CI/CD pipeline with Gitea Actions and secrets configuration --- .gitea/workflows/AUTO_SETUP_SECRETS.md | 86 ++++ .gitea/workflows/CI_CD_CHECKLIST.md | 90 ++++ .gitea/workflows/MANUAL_SETUP_SIMPLE.md | 101 ++++ .gitea/workflows/MANUAL_SETUP_WITH_TOKEN.md | 87 ++++ .gitea/workflows/NEXT_STEPS.md | 119 +++++ .gitea/workflows/QUICK_SECRETS_SETUP.md | 104 ++++ .gitea/workflows/REGISTRY_INFO.md | 30 ++ .gitea/workflows/SECRETS_SETUP.md | 86 ++++ .gitea/workflows/TEST_WORKFLOW.md | 132 ++++++ .gitea/workflows/TOKEN_ISSUE_FIX.md | 75 +++ .gitea/workflows/ci-cd.yml | 102 ---- .gitea/workflows/ci.yml | 51 -- .../workflows/update-production-secrets.yml | 74 +++ .../ansible/playbooks/deploy-update.yml | 98 ++-- deployment/ansible/playbooks/rollback.yml | 166 +++++++ .../playbooks/setup-infrastructure.yml | 210 +++++++++ .../playbooks/setup-production-secrets.yml | 92 ++++ .../playbooks/setup-ssl-certificates.yml | 86 ++++ deployment/ansible/playbooks/sync-stacks.yml | 53 +++ scripts/deploy-production.sh | 83 ---- .../populate_images_from_filesystem.php | 2 +- scripts/prepare-secrets.sh | 34 ++ scripts/production-deploy.sh | 446 ------------------ scripts/server-deploy.sh | 240 ---------- scripts/setup-gitea-secrets-interactive.sh | 123 +++++ scripts/setup-gitea-secrets-with-token.sh | 120 +++++ scripts/setup-gitea-secrets.sh | 96 ++++ scripts/setup-production-secrets.sh | 85 ++++ 28 files changed, 2113 insertions(+), 958 deletions(-) create mode 100644 .gitea/workflows/AUTO_SETUP_SECRETS.md create mode 100644 .gitea/workflows/CI_CD_CHECKLIST.md create mode 100644 .gitea/workflows/MANUAL_SETUP_SIMPLE.md create mode 100644 .gitea/workflows/MANUAL_SETUP_WITH_TOKEN.md create mode 100644 .gitea/workflows/NEXT_STEPS.md create mode 100644 .gitea/workflows/QUICK_SECRETS_SETUP.md create mode 100644 .gitea/workflows/REGISTRY_INFO.md create mode 100644 .gitea/workflows/SECRETS_SETUP.md create mode 100644 .gitea/workflows/TEST_WORKFLOW.md create mode 100644 .gitea/workflows/TOKEN_ISSUE_FIX.md delete mode 100644 .gitea/workflows/ci-cd.yml delete mode 100644 .gitea/workflows/ci.yml create mode 100644 .gitea/workflows/update-production-secrets.yml create mode 100644 deployment/ansible/playbooks/rollback.yml create mode 100644 deployment/ansible/playbooks/setup-infrastructure.yml create mode 100644 deployment/ansible/playbooks/setup-production-secrets.yml create mode 100644 deployment/ansible/playbooks/setup-ssl-certificates.yml create mode 100644 deployment/ansible/playbooks/sync-stacks.yml delete mode 100755 scripts/deploy-production.sh create mode 100755 scripts/prepare-secrets.sh delete mode 100755 scripts/production-deploy.sh delete mode 100644 scripts/server-deploy.sh create mode 100755 scripts/setup-gitea-secrets-interactive.sh create mode 100755 scripts/setup-gitea-secrets-with-token.sh create mode 100755 scripts/setup-gitea-secrets.sh create mode 100755 scripts/setup-production-secrets.sh diff --git a/.gitea/workflows/AUTO_SETUP_SECRETS.md b/.gitea/workflows/AUTO_SETUP_SECRETS.md new file mode 100644 index 00000000..87e4b797 --- /dev/null +++ b/.gitea/workflows/AUTO_SETUP_SECRETS.md @@ -0,0 +1,86 @@ +# Automatisches Secrets-Setup + +## Schnellstart + +```bash +# Interaktives Script (fragt nach Token) +bash scripts/setup-gitea-secrets-interactive.sh +``` + +Das Script: +1. Fragt nach einem Gitea Access Token +2. Setzt automatisch alle drei Secrets via API + +## Token generieren + +Falls noch kein Token vorhanden: + +1. **Gehe zu Gitea Settings:** + ``` + https://git.michaelschiemer.de/user/settings/applications + ``` + +2. **Klicke "Generate New Token"** + +3. **Konfiguration:** + - Name: `secrets-setup` (oder beliebig) + - Disponível scopes: + - ✅ `write:repository` (mindestens) + - ✅ Oder wähle alle für volle Berechtigung + +4. **Kopiere den Token** (wird nur einmal angezeigt!) + +## Script ausführen + +```bash +# Script starten +bash scripts/setup-gitea-secrets-interactive.sh + +# Token eingeben (wird nicht angezeigt) +Gitea Token: + +# Script setzt automatisch: +# ✅ REGISTRY_USER = admin +# ✅ REGISTRY_PASSWORD = registry-secure-password-3125 +# ✅ SSH_PRIVATE_KEY = +``` + +## Verifizierung + +Nach erfolgreichem Setup: + +1. **Prüfe in Gitea UI:** + ``` + https://git.michaelschiemer.de/michael/michaelschiemer/settings/secrets/actions + ``` + +2. **Sollte zeigen:** + - ✅ REGISTRY_USER + - ✅ REGISTRY_PASSWORD + - ✅ SSH_PRIVATE_KEY + +Alle drei Secrets sollten "Hidden" als Wert anzeigen. + +## Troubleshooting + +### "Token ungültig" +- Prüfe, ob Token korrekt kopiert wurde (keine Leerzeichen) +- Prüfe, ob Token die Berechtigung `write:repository` hat + +### "Repository nicht gefunden" +- Prüfe Repository-Name: `michael/michaelschiemer` +- Prüfe, ob du Zugriff auf das Repository hast + +### "HTTP 403 Forbidden" +- Token hat keine ausreichenden Berechtigungen +- Generiere neuen Token mit `write:repository` scope + +### API nicht erreichbar +- Prüfe Gitea URL: `https://git.michaelschiemer.de` +- Prüfe Netzwerkverbindung + +## Alternative: Manuelles Setup + +Falls das automatische Setup nicht funktioniert: + +Siehe `.gitea/workflows/QUICK_SECRETS_SETUP.md` für manuelle Anleitung. diff --git a/.gitea/workflows/CI_CD_CHECKLIST.md b/.gitea/workflows/CI_CD_CHECKLIST.md new file mode 100644 index 00000000..740dca8b --- /dev/null +++ b/.gitea/workflows/CI_CD_CHECKLIST.md @@ -0,0 +1,90 @@ +# CI/CD Pipeline - Setup Checklist + +## ✅ Status + +### 1. Repository Secrets in Gitea ✅ BEREIT +**Pfad**: Repository → Settings → Secrets + +**Schnelles Setup:** +```bash +bash scripts/prepare-secrets.sh +``` + +**Erforderliche Secrets:** +- [ ] **REGISTRY_USER**: `admin` +- [ ] **REGISTRY_PASSWORD**: `registry-secure-password-2025` +- [ ] **SSH_PRIVATE_KEY**: Vollständiger Inhalt von `~/.ssh/production` + +**Details**: Siehe `.gitea/workflows/QUICK_SECRETS_SETUP.md` + +**Hinweis**: Alle Secrets müssen in Gitea konfiguriert werden, bevor die Pipeline läuft. + +### 2. Docker Registry ✅ +- ✅ Registry läuft auf `git.michaelschiemer.de:5000` +- ✅ Authentifizierung konfiguriert (admin/registry-secure-password-2025) +- ✅ Erreichbar via HTTP auf `127.0.0.1:5000` +- ✅ Image `framework` bereits vorhanden + +**Registry-Test**: +```bash +curl -u admin:registry-secure-password-2025 http://127.0.0.1:5000/v2/_catalog +# Erwartete Ausgabe: {"repositories":["framework"]} +``` + +### 3. Ansible Playbooks ✅ +- ✅ `deploy-update.yml` - Aktualisiert für Docker Compose +- ✅ `rollback.yml` - Aktualisiert für Docker Compose +- ✅ `setup-infrastructure.yml` - Enthält Registry-Setup +- ✅ `sync-stacks.yml` - Synchronisiert Stack-Konfigurationen + +### 4. Workflow-Konfiguration ✅ +- ✅ `.gitea/workflows/production-deploy.yml` - Haupt-Workflow +- ✅ `Dockerfile.production` - Production Dockerfile erstellt +- ✅ Application Stack konfiguriert (`deployment/stacks/application/docker-compose.yml`) + +## 📋 Nächste Schritte + +### Schritt 1: Secrets in Gitea hinzufügen +Siehe `.gitea/workflows/SECRETS_SETUP.md` für detaillierte Anleitung. + +### Schritt 2: Workflow manuell testen +1. Gehe zu: `https://git.michaelschiemer.de//michaelschiemer/actions` +2. Wähle "Production Deployment Pipeline" +3. Klicke "Run workflow" +4. Wähle Branch `main` +5. Beobachte die Ausführung + +### Schritt 3: Automatisches Deployment testen +1. Push zu `main` Branch +2. Workflow sollte automatisch starten +3. Prüfe Logs in Gitea Actions + +## 🔧 Troubleshooting + +### Registry nicht erreichbar +```bash +# Prüfe Registry-Status +ssh deploy@94.16.110.151 "docker ps | grep registry" + +# Teste Registry-Erreichbarkeit +ssh deploy@94.16.110.151 "curl -u admin:registry-secure-password-2025 http://127.0.0.1:5000/v2/_catalog" +``` + +### Workflow schlägt bei Registry-Login fehl +- Prüfe, ob `REGISTRY_USER` und `REGISTRY_PASSWORD` Secrets korrekt gesetzt sind +- Prüfe, ob das Passwort in Gitea mit dem Server übereinstimmt + +### Image Pull schlägt fehl +- Prüfe, ob das Image in der Registry existiert: + ```bash + curl -u admin:registry-secure-password-2025 http://127.0.0.1:5000/v2/framework/tags/list + ``` +- Prüfe Registry-Logs für Fehler + +### Deployment schlägt fehl +- Prüfe Ansible-Logs im Workflow +- Prüfe, ob `deployment/stacks/application/docker-compose.yml` auf dem Server existiert +- Prüfe Docker-Logs auf dem Server: + ```bash + ssh deploy@94.16.110.151 "cd ~/deployment/stacks/application && docker compose logs" + ``` diff --git a/.gitea/workflows/MANUAL_SETUP_SIMPLE.md b/.gitea/workflows/MANUAL_SETUP_SIMPLE.md new file mode 100644 index 00000000..f64a27dc --- /dev/null +++ b/.gitea/workflows/MANUAL_SETUP_SIMPLE.md @@ -0,0 +1,101 @@ +# Manuelles Secrets-Setup - Einfache Anleitung + +Da das automatische Setup Probleme macht, hier die manuelle Lösung: + +## Schritt 1: Gehe zu Gitea Secrets-Seite + +Öffne im Browser: +``` +https://git.michaelschiemer.de/michael/michaelschiemer/settings/secrets/actions +``` + +**Hinweis**: Falls diese Seite nicht existiert oder einen 404 gibt: +- Prüfe, ob das Repository wirklich `michael/michaelschiemer` heißt +- Prüfe, ob du Zugriff auf das Repository hast +- Prüfe, ob Actions aktiviert ist + +## Schritt 2: Füge die drei Secrets hinzu + +Für jedes Secret: **Klicke "New Secret"**, fülle aus, **Save** + +### Secret 1: REGISTRY_USER + +- **Name**: `REGISTRY_USER` +- **Value**: `admin` +- **Save** + +### Secret 2: REGISTRY_PASSWORD + +- **Name**: `REGISTRY_PASSWORD` +- **Value**: `registry-secure-password-2025` +- **Save** + +### Secret 3: SSH_PRIVATE_KEY + +- **Name**: `SSH_PRIVATE_KEY` +- **Value**: Führe aus `cat ~/.ssh/production` und kopiere den KOMPLETTEN Inhalt + ```bash + cat ~/.ssh/production + ``` + + **Wichtig**: Kopiere ALLES, inklusive: + - `-----BEGIN OPENSSH PRIVATE KEY-----` + - Alle Zeilen dazwischen + - `-----END OPENSSH PRIVATE KEY-----` + +- **Save** + +## Schritt 3: Verifizierung + +Nach dem Setup sollten alle drei Secrets in der Liste erscheinen: +- ✅ REGISTRY_USER +- ✅ REGISTRY_PASSWORD +- ✅ SSH_PRIVATE_KEY + +Alle zeigen "Hidden" als Wert. + +## Falls die Secrets-Seite nicht erreichbar ist + +### Option A: Repository-Name prüfen + +```bash +# Prüfe aktuelle Remote-URL +git remote get-url origin + +# Sollte zeigen: +# https://git.michaelschiemer.de//.git +``` + +Falls der Name anders ist, verwende die korrekte URL. + +### Option B: Repository erstellen + +Falls das Repository noch nicht existiert: + +1. Gehe zu: `https://git.michaelschiemer.de/repos/new` +2. Erstelle das Repository `michaelschiemer` +3. Dann gehe zu: `https://git.michaelschiemer.de/michael/michaelschiemer/settings/secrets/actions` + +### Option C: Actions aktivieren + +Falls Actions nicht aktiviert ist: + +1. Gehe zu Repository Settings +2. Prüfe, ob "Actions" aktiviert ist +3. Falls nicht, aktiviere es in den Repository-Einstellungen + +## Nächster Schritt + +Nach erfolgreichem Setup der Secrets: + +1. **Teste den Workflow:** + ``` + https://git.michaelschiemer.de/michael/michaelschiemer/actions + ``` + +2. **Oder pushe einen Commit:** + ```bash + git push origin main + ``` + +Der Workflow sollte dann automatisch starten. diff --git a/.gitea/workflows/MANUAL_SETUP_WITH_TOKEN.md b/.gitea/workflows/MANUAL_SETUP_WITH_TOKEN.md new file mode 100644 index 00000000..244d5921 --- /dev/null +++ b/.gitea/workflows/MANUAL_SETUP_WITH_TOKEN.md @@ -0,0 +1,87 @@ +# Secrets Setup mit Token - Schritt für Schritt + +## ✅ Du hast bereits ein Token - Perfekt! + +## Option 1: Automatisches Setup (Empfohlen) + +Führe einfach dieses Kommando aus (ersetze `` mit deinem Token): + +```bash +bash scripts/setup-gitea-secrets-with-token.sh +``` + +**Beispiel:** +```bash +bash scripts/setup-gitea-secrets-with-token.sh ghp_1234567890abcdefghijklmnopqrstuvwxyz +``` + +Das Script: +1. ✅ Testet die API-Verbindung +2. ✅ Setzt automatisch `REGISTRY_USER` = `admin` +3. ✅ Setzt automatisch `REGISTRY_PASSWORD` = `registry-secure-password-2025` +4. ✅ Setzt automatisch `SSH_PRIVATE_KEY` = Inhalt von `~/.ssh/production` + +## Option 2: Manuell über Gitea UI + +Falls das automatische Setup nicht funktioniert: + +### Schritt 1: Gehe zu Secrets-Seite + +``` +https://git.michaelschiemer.de/michael/michaelschiemer/settings/secrets/actions +``` + +### Schritt 2: Füge jedes Secret einzeln hinzu + +**REGISTRY_USER:** +1. Klicke "New Secret" +2. Name: `REGISTRY_USER` +3. Value: `admin` +4. Save + +**REGISTRY_PASSWORD:** +1. Klicke "New Secret" +2. Name: `REGISTRY_PASSWORD` +3. Value: `registry-secure-password-2025` +4. Save + +**SSH_PRIVATE_KEY:** +1. Klicke "New Secret" +2. Name: `SSH_PRIVATE_KEY` +3. Value: Kompletter Inhalt von `~/.ssh/production` + ```bash + cat ~/.ssh/production + ``` + (Kopiere ALLES, inklusive `-----BEGIN` und `-----END` Zeilen) +4. Save + +## Verifizierung + +Nach dem Setup: + +1. **Prüfe in Gitea UI:** + ``` + https://git.michaelschiemer.de/michael/michaelschiemer/settings/secrets/actions + ``` + +2. **Sollte zeigen:** + - ✅ REGISTRY_USER + - ✅ REGISTRY_PASSWORD + - ✅ SSH_PRIVATE_KEY + +Alle drei Secrets sollten "Hidden" als Wert anzeigen. + +## Troubleshooting + +### "API-Verbindung fehlgeschlagen" +- Prüfe Token-Kopierung (keine Leerzeichen) +- Prüfe Token-Berechtigung: `write:repository` Scope nötig +- Prüfe Repository-Name: `michael/michaelschiemer` + +### "HTTP 403 Forbidden" +- Token hat keine ausreichenden Berechtigungen +- Generiere neuen Token mit `write:repository` scope + +### "HTTP 404 Not Found" +- Repository existiert nicht oder falscher Name +- Prüfe: `https://git.michaelschiemer.de/michael/michaelschiemer` diff --git a/.gitea/workflows/NEXT_STEPS.md b/.gitea/workflows/NEXT_STEPS.md new file mode 100644 index 00000000..ea2f8d9b --- /dev/null +++ b/.gitea/workflows/NEXT_STEPS.md @@ -0,0 +1,119 @@ +# ✅ Secrets erfolgreich gesetzt! + +## Status + +✅ Repository erstellt +✅ Secrets konfiguriert: +- REGISTRY_USER +- REGISTRY_PASSWORD +- SSH_PRIVATE_KEY + +## 🚀 Nächster Schritt: CI/CD Workflow testen + +### Option 1: Workflow manuell triggern (Empfohlen) + +1. **Gehe zu Actions:** + ``` + https://git.michaelschiemer.de/michael/michaelschiemer/actions + ``` + +2. **Workflow auswählen:** + - Suche nach "Production Deployment Pipeline" + - Klicke auf den Workflow + +3. **Workflow starten:** + - Klicke "Run workflow" (rechts oben) + - Branch: `main` + - `skip_tests`: deaktiviert (Tests sollen laufen) + - Klicke "Run workflow" + +4. **Logs beobachten:** + - Prüfe jeden Schritt in den Logs + - Er_warte ~8-15 Minuten für komplette Ausführung + +### Option 2: Automatisches Deployment via Commit + +```bash +# Stelle sicher, dass alles committed ist +git add . +git commit -m "chore: CI/CD pipeline setup complete" + +# Push zu main - startet automatisch den Workflow +git push origin main +``` + +## 📊 Workflow-Schritte + +Der Workflow führt folgende Schritte aus: + +1. **Tests** (~2-5 Min) + - PHP Setup + - Composer Dependencies + - Tests ausführen + +2. **Build** (~3-5 Min) + - Multi-Stage Docker Build + - Composer Dependencies (Production) + - Frontend Build (npm) + - Final Production Image + +3. **Push** (~1-2 Min) + - Docker Login zur Registry + - Image Tag generieren + - Image zur Registry pushen + +4. **Deploy** (~2-4 Min) + - Ansible Playbook ausführen + - Image auf Production pullen + - Application Stack aktualisieren + - Services neu starten + +**Gesamtzeit: ~8-15 Minuten** + +## ✅ Erfolgreiche Ausführung erkennen + +Der Workflow ist erfolgreich, wenn: + +- ✅ Alle Jobs grün sind +- ✅ Keine Fehler in den Logs +- ✅ "Deploy via Ansible" erfolgreich +- ✅ Application läuft auf Production-Server + +## 🔍 Verifizierung nach Deployment + +```bash +# Prüfe Application-Status +ssh deploy@94.16.110.151 "cd ~/deployment/stacks/application && docker compose ps" + +# Prüfe Logs +ssh deploy@94.16.110.151 "cd ~/deployment/stacks/application && docker compose logs --tail=50" + +# Prüfe Health Endpoint +curl -k https://michaelschiemer.de/health +``` + +## 🐛 Troubleshooting + +### Workflow startet nicht +- Prüfe, ob `.gitea/workflows/production-deploy.yml` im Repository ist +- Prüfe Workflow-Syntax + +### "Secret not found" Fehler +- Prüfe Secrets-Seite: `https://git.michaelschiemer.de/michael/michaelschiemer/settings/secrets/actions` +- Prüfe, ob Namen exakt übereinstimmen (Groß-/Kleinschreibung!) + +### Registry Login fehlgeschlagen +- Prüfe `REGISTRY_USER` und `REGISTRY_PASSWORD` Secrets +- Teste Registry: `curl -u admin:registry-secure-password-2025 http://127.0.0.1:5000/v2/_catalog` + +### Deployment fehlgeschlagen +- Prüfe `SSH_PRIVATE_KEY` Secret +- Prüfe Ansible-Verbindung: `ssh deploy@94.16.110.151 "echo OK"` +- Prüfe Server-Logs + +## 🎉 Nach erfolgreichem Test + +Du kannst jetzt: +- ✅ Commits pushen → Automatisches Deployment +- ✅ Workflow manuell triggern für kontrollierte Deployments +- ✅ Branch-Protection aktivieren für sichere Deployments diff --git a/.gitea/workflows/QUICK_SECRETS_SETUP.md b/.gitea/workflows/QUICK_SECRETS_SETUP.md new file mode 100644 index 00000000..586ccc5a --- /dev/null +++ b/.gitea/workflows/QUICK_SECRETS_SETUP.md @@ -0,0 +1,104 @@ +# Quick Secrets Setup für Gitea CI/CD + +## Zusammenfassung der benötigten Werte + +### REGISTRY_USER +``` +admin +``` + +### REGISTRY_PASSWORD +``` +registry-secure-password-2025 +``` + +### SSH_PRIVATE_KEY +Kopiere den kompletten Inhalt von φ~/.ssh/production`: + +```bash +# Zeige SSH Key Inhalt (für Copy-Paste) +cat ~/.ssh/production +``` + +**Wichtig**: Der komplette Inhalt muss kopiert werden, inklusive: +- `-----BEGIN OPENSSH PRIVATE KEY-----` +- Alle Zeilen dazwischen +- `-----END OPENSSH PRIVATE KEY-----` + +## Schnelle Vorbereitung + +Führe das Helper-Script aus, um alle Werte anzuzeigen: + +```bash +bash scripts/prepare-secrets.sh +``` + +Dies zeigt alle drei Secrets an, die du kopieren kannst. + +## Manuelle Einrichtung in Gitea + +1. **Gehe zu Repository Settings:** + ``` + https://git.michaelschiemer.de//michaelschiemer/settings/secrets + ``` + +2. **Klicke auf "New Secret"** + +3. **Füge jedes Secret hinzu:** + + **REGISTRY_USER:** + - Name: `REGISTRY_USER` + - Value: `admin` + - Save + + **REGISTRY_PASSWORD:** + - Name: `REGISTRY_PASSWORD` + - Value: `registry-secure-password-2025` + - Save + + **SSH_PRIVATE_KEY:** + - Name: `SSH_PRIVATE_KEY` + - Value: `` + - Save + +## Verifizierung + +Nach dem Setup sollten alle drei Secrets in der Liste erscheinen: +- ✅ REGISTRY_USER +- ✅ REGISTRY_PASSWORD +- ✅ SSH_PRIVATE_KEY + +Alle zeigen "Hidden" als Wert. + +## Nächster Schritt + +Sobald die Secrets konfiguriert sind: + +1. **Workflow manuell testen:** + ``` + https://git.michaelschiemer.de//michaelschiemer/actions + ``` + - Wähle "Production Deployment Pipeline" + - Klicke "Run workflow" + - Wähle Branch `main` + +2. **Oder automatisches Deployment testen:** + - Pushe einen Commit zu `main` + - Workflow startet automatisch + +## Troubleshooting + +### Secrets werden nicht erkannt +- Prüfe, ob die Namen exakt übereinstimmen (Groß-/Kleinschreibung!) +- Prüfe, ob keine Leerzeichen am Anfang/Ende + +### SSH Key Fehler +- Stelle sicher, dass der komplette Key kopiert wurde +- Prüfe, dass `-----BEGIN` und `-----END` Zeilen enthalten sind + +### Registry Login Fehler +- Prüfe, ob `REGISTRY_USER` und `REGISTRY_PASSWORD` korrekt sind +- Teste Registry-Erreichbarkeit: + ```bash + curl -u admin:registry-secure-password-2025 http://127.0.0.1:5000/v2/_catalog + ``` diff --git a/.gitea/workflows/REGISTRY_INFO.md b/.gitea/workflows/REGISTRY_INFO.md new file mode 100644 index 00000000..d4bd3b43 --- /dev/null +++ b/.gitea/workflows/REGISTRY_INFO.md @@ -0,0 +1,30 @@ +# Docker Registry Information + +## Registry Details + +- **URL**: `git.michaelschiemer.de:5000` (intern) oder `registry.michaelschiemer.de` (via Traefik) +- **Standard Credentials**: + - **Username**: `admin` + - **Password**: `registry-secure-password-2025` + +⚠️ **WICHTIG**: Das Passwort sollte in Produktion geändert werden! + +## Für Gitea Secrets + +Verwende folgende Werte in den Gitea Repository Secrets: + +- **REGISTRY_USER**: `admin` +- **REGISTRY_PASSWORD**: `registry-secure-password-2025` (oder das aktuell gesetzte Passwort) + +## Registry Test + +```bash +# Login testen +echo "registry-secure-password-2025" | docker login git.michaelschiemer.de:5000 -u admin --password-stdin + +# Images auflisten +curl -u admin:registry-secure-password-2025 http://git.michaelschiemer.de:5000/v2/_catalog + +# Oder via Traefik (HTTPS) +curl -u admin:registry-secure-password-2025 https://registry.michaelschiemer.de/v2/_catalog +``` diff --git a/.gitea/workflows/SECRETS_SETUP.md b/.gitea/workflows/SECRETS_SETUP.md new file mode 100644 index 00000000..78a57149 --- /dev/null +++ b/.gitea/workflows/SECRETS_SETUP.md @@ -0,0 +1,86 @@ +# Gitea Repository Secrets Setup + +## Erforderliche Secrets + +Diese Secrets müssen in Gitea konfiguriert werden unter: +**Repository → Settings → Secrets** + +### 1. REGISTRY_USER +- **Beschreibung**: Benutzername für Docker Registry Login +- **Typ**: String +- **Wert**: Standardmäßig `admin` oder der Benutzername für die Registry +- **Verwendung**: Docker Registry Authentication beim Image Push + +### 2. REGISTRY_PASSWORD +- **Beschreibung**: Passwort für Docker Registry Login +- **Typ**: Password (versteckt) +- **Wert**: Das Passwort für die Docker Registry auf `git.michaelschiemer.de:5000` +- **Verwendung**: Docker Registry Authentication beim Image Push + +### 3. SSH_PRIVATE_KEY +- **Beschreibung**: SSH Private Key für Zugriff auf Production Server +- **Typ**: SSH Key (versteckt) +- **Wert**: Der komplette Inhalt der SSH-Private-Key-Datei (~/.ssh/production) +- **Verwendung**: SSH-Verbindung zum Production-Server für Ansible Deployment + +## Setup-Anleitung + +### Schritt 1: SSH Key erstellen/exportieren + +```bash +# Falls noch nicht vorhanden, SSH Key für Production erstellen +ssh-keygen -t ed25519 -f ~/.ssh/production -C "gitea-ci-cd" + +# SSH Key Inhalt anzeigen (für Copy-Paste) +cat ~/.ssh/production +``` + +**⚠️ Wichtig**: Der komplette Inhalt der Datei (inkl. `-----BEGIN OPENSSH PRIVATE KEY-----` und `-----END OPENSSH PRIVATE KEY-----`) muss in das Secret eingefügt werden. + +### Schritt 2: Docker Registry Credentials prüfen + +Die Registry läuft auf dem Production-Server. Prüfe die Credentials: + +```bash +# SSH zum Production-Server +ssh deploy@94.16.110.151 + +# Prüfe, ob Registry läuft +docker ps | grep registry + +# Prüfe Registry-Konfiguration (falls vorhanden) +cat ~/deployment/stacks/registry/docker-compose.yml 2>/dev/null || echo "Registry Config nicht gefunden" +``` + +**Hinweis**: Falls die Registry noch nicht konfiguriert ist, müssen die Credentials festgelegt werden. + +### Schritt 3: Secrets in Gitea hinzufügen + +1. Gehe zu: `https://git.michaelschiemer.de//michaelschiemer/settings/secrets` +2. Klicke auf **"Add Secret"** +3. Füge jedes Secret einzeln hinzu: + + **REGISTRY_USER**: + - Name: `REGISTRY_USER` + - Value: `admin` (oder der tatsächliche Registry-Benutzername) + - Save + + **REGISTRY_PASSWORD**: + - Name: `REGISTRY_PASSWORD` + - Value: `` + - Save + + **SSH_PRIVATE_KEY**: + - Name: `SSH_PRIVATE_KEY` + - Value: `` + - Save + +### Schritt 4: Secrets verifizieren + +Nach dem Hinzufügen sollten alle drei Secrets in der Liste erscheinen mit "Hidden" als Wert. + +**✅ Checkliste**: +- [ ] REGISTRY_USER hinzugefügt +- [ ] REGISTRY_PASSWORD hinzugefügt +- [ ] SSH_PRIVATE_KEY hinzugefügt +- [ ] Alle Secrets zeigen "Hidden" als Wert diff --git a/.gitea/workflows/TEST_WORKFLOW.md b/.gitea/workflows/TEST_WORKFLOW.md new file mode 100644 index 00000000..0ddbfe14 --- /dev/null +++ b/.gitea/workflows/TEST_WORKFLOW.md @@ -0,0 +1,132 @@ +# CI/CD Workflow Testen + +## ✅ Secrets sind konfiguriert - Perfekt! + +## Option 1: Workflow manuell triggern (Empfohlen) + +### Schritt 1: Gehe zu Actions + +Öffne im Browser: +``` +https://git.michaelschiemer.de/michael/michaelschiemer renewal/actions +``` + +### Schritt 2: Workflow auswählen + +1. Suche nach "Production Deployment Pipeline" +2. Klicke auf den Workflow + +### Schritt 3: Workflow manuell starten + +1. Klicke auf "Run workflow" (rechts oben) +2. Wähle Branch: `main` +3. Optional: `skip_tests` deaktiviert lassen (Tests sollen laufen) +4. Klicke "Run workflow" + +### Schritt 4: Logs beobachten + +Der Workflow führt folgende Schritte aus: + +1. **Checkout code** - Code wird ausgecheckt +2. **Run Tests** - PHP Tests werden ausgeführt +3. **Build Docker Image** - Docker Image wird gebaut +4. **Push to Registry** - Image wird zur Registry gepusht +5. **Deploy via Ansible** - Deployment auf Production-Server + +**Beobachte die Logs** und prüfe jeden Schritt! + +## Option 2: Automatisches Deployment via Commit + +### Test-Commit pushen + +```bash +# Stelle sicher, dass alles committed ist +git add . +git commit -m "test: CI/CD workflow test" || echo "Keine Änderungen" + +# Push zu main Branch +git push origin main +``` + +Der Workflow startet automatisch nach dem Push. + +## Was passiert beim Workflow? + +### 1. Tests (ca. 2-5 Minuten) +- PHP Version Setup +- Composer Dependencies installieren +- Tests ausführen + +### 2. Build (ca. 3-5 Minuten) +Live - Multi-Stage Docker Build: +- Composer Dependencies (Production) +- Frontend Build (npm) +- Finales Production Image + +### 3. Push (ca. 1-2 Minuten) +- Docker Login zur Registry +- Image Tag generieren (SHA + Timestamp) +- Image zur Registry pushen + +### 4. Deploy (ca. 2-4 Minuten) +- Ansible Playbook ausführen +- Image auf Production-Server pullen +- Application Stack aktualisieren +- Services neu starten + +**Gesamtzeit: ~8-15 Minuten** + +## Workflow-Status prüfen + +### In Gitea UI: +``` +https://git.michaelschiemer.de/michael/michaelschiemer/actions +``` + +### Via Command Line: +```bash +# Prüfe ob Workflow läuft +curl -s -H "Authorization: token " \ + "https://git.michaelschiemer.de/api/v1/repos/michael/michaelschiemer/actions/runs" | \ + jq '.workflow_runs[0] | {status, conclusion, created_at}' +``` + +## Erfolgreiche Ausführung erkennen + +Der Workflow ist erfolgreich, wenn: + +✅ Alle Jobs grün sind +✅ Keine Fehler in den Logs +✅ Letzter Schritt "Deploy via Ansible" erfolgreich +✅ Application läuft auf Production-Server + +## Troubleshooting + +### Workflow startet nicht +- Prüfe, ob `.gitea/workflows/production-deploy.yml` im Repository ist +- Prüfe, ob Workflow-Syntax korrekt ist +- Prüfe Gitea Actions ist aktiviert + +### "Secret not found" Fehler +- Prüfe, ob alle drei Secrets gesetzt sind +- Prüfe, ob Namen exakt übereinstimmen (Groß-/Kleinschreibung!) + +### Registry Login fehlgeschlagen +- Prüfe `REGISTRY_USER` und `REGISTRY_PASSWORD` Secrets +- Prüfe Registry erreichbar: `curl -u admin:registry-secure-password-2025 http://127.0.0.1:5000/v2/_catalog` + +### Deployment fehlgeschlagen +- Prüfe `SSH_PRIVATE_KEY` Secret +- Prüfe Ansible-Verbindung zum Server +- Prüfe Server-Logs: `ssh deploy@94.16.110.151 "docker compose -f ~/deployment/stacks/application/docker-compose.yml logs"` + +## Nächste Schritte nach erfolgreichem Test + +1. ✅ Workflow funktioniert +2. ✅ Automatisches Deployment getestet +3. ✅ Production-Stack läuft + +**Du kannst jetzt:** +- Normale Commits pushen → Automatisches Deployment +- Workflow manuell triggern für kontrollierte Deployments +- Branch-Protection aktivieren für sichere Deployments diff --git a/.gitea/workflows/TOKEN_ISSUE_FIX.md b/.gitea/workflows/TOKEN_ISSUE_FIX.md new file mode 100644 index 00000000..bd95ea81 --- /dev/null +++ b/.gitea/workflows/TOKEN_ISSUE_FIX.md @@ -0,0 +1,75 @@ +# Token-Probleme beheben + +## Problem + +Das Token hat nicht die richtigen Scopes oder das Repository wurde nicht gefunden. + +## Lösung: Neuen Token mit richtigen Scopes erstellen + +### Schritt 1: Token neu generieren + +1. **Gehe zu Gitea Settings:** + ``` + https://git.michaelschiemer.de/user/settings/applications + ``` + +2. **Falls bereits ein Token existiert:** + - Lösche den alten Token (falls nötig) + - Oder erstelle einen neuen mit anderen Namen + +3. **Klicke "Generate New Token"** + +4. **WICHTIG - Diese Scopes aktivieren:** + - ✅ `read:user` (mindestens erforderlich) + - ✅ `write:repository` (für Secrets schreiben) + - ✅ Oder wähle **alle Scopes** für volle Berechtigung + +5. **Token kopieren** (wird nur einmal angezeigt!) + +### Schritt 2: Repository-Name prüfen + +Prüfe, ob das Repository wirklich `michael/michaelschiemer` heißt: + +```bash +# Prüfe Remote-URL +git remote get-url origin + +# Sollte zeigen: +# https://git.michaelschiemer.de/michael/michaelschiemer.git +``` + +Falls der Name anders ist, setze die Umgebungsvariable: +```bash +REPO_OWNER= REPO_NAME= bash scripts/setup-gitea-secrets-with-token.sh +``` + +### Schritt 3: Script erneut ausführen + +```bash +bash scripts/setup-gitea-secrets-with-token.sh +``` + +## Alternative: Manuelles Setup über UI + +Falls das automatische Setup weiterhin Probleme macht: + +1. **Gehe zu:** + ``` + https://git.michaelschiemer.de/michael/michaelschiemer/settings/secrets/actions + ``` + +2. **Füge manuell hinzu:** + - `REGISTRY_USER` = `admin` + - `REGISTRY_PASSWORD` = `registry-secure-password-2025` + - `SSH_PRIVATE_KEY` = `cat ~/.ssh/production` + +## Troubleshooting + +### "token does not have at least one of required scope(s)" +→ Token benötigt `read:user` Scope - neuen Token mit diesem Scope generieren + +### "The target couldn't be found" (404) +→ Repository existiert nicht oder falscher Name - prüfe Repository-URL + +### "404 page not found" bei Secrets-Endpoint +→ Actions möglicherweise nicht aktiviert - prüfe in Gitea Admin-Panel diff --git a/.gitea/workflows/ci-cd.yml b/.gitea/workflows/ci-cd.yml deleted file mode 100644 index 1ab7963d..00000000 --- a/.gitea/workflows/ci-cd.yml +++ /dev/null @@ -1,102 +0,0 @@ -name: CI/CD Pipeline für michaelschiemer.de - -on: - push: - branches: [ main, develop ] - pull_request: - branches: [ main ] - -env: - REGISTRY_URL: docker-registry:5000 # Container network access - IMAGE_NAME: michaelschiemer - -jobs: - test: - runs-on: ubuntu-latest - container: - image: catthehacker/ubuntu:act-latest - services: - redis: - image: redis:8-alpine - mariadb: - image: mariadb:latest - env: - MYSQL_ROOT_PASSWORD: test - MYSQL_DATABASE: test - - steps: - - name: Checkout Code - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '18' - - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: '8.4' - extensions: gd, zip, pdo, pdo_mysql, opcache, pcntl, posix, shmop, redis - tools: composer - - - name: Install Dependencies - run: composer install --no-progress --prefer-dist --optimize-autoloader - - - name: Build Frontend Assets - run: npm install && npm run build - - - name: Run Tests - run: ./vendor/bin/pest - env: - DB_HOST: mariadb - DB_PORT: 3306 - DB_DATABASE: test - DB_USERNAME: root - DB_PASSWORD: test - REDIS_HOST: redis - REDIS_PORT: 6379 - - build: - needs: test - runs-on: ubuntu-latest - container: - image: catthehacker/ubuntu:act-latest - if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' - - steps: - - name: Checkout Code - uses: actions/checkout@v4 - - - name: Login to Private Registry - run: < /dev/null | - echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login ${{ env.REGISTRY_URL }} -u admin --password-stdin - - - name: Determine Image Tag - id: tag - run: | - if [ "${{ github.ref }}" = "refs/heads/main" ]; then - echo "tag=latest" >> $GITHUB_OUTPUT - else - echo "tag=develop" >> $GITHUB_OUTPUT - fi - - - name: Build and Push Images - run: | - # Build and push PHP image - if [ -f docker/php/Dockerfile ]; then - docker build -t ${{ env.REGISTRY_URL }}/${{ env.IMAGE_NAME }}/php:${{ steps.tag.outputs.tag }} -f docker/php/Dockerfile . - docker push ${{ env.REGISTRY_URL }}/${{ env.IMAGE_NAME }}/php:${{ steps.tag.outputs.tag }} - fi - - # Build and push Nginx image - if [ -f docker/nginx/Dockerfile ]; then - docker build -t ${{ env.REGISTRY_URL }}/${{ env.IMAGE_NAME }}/nginx:${{ steps.tag.outputs.tag }} -f docker/nginx/Dockerfile . - docker push ${{ env.REGISTRY_URL }}/${{ env.IMAGE_NAME }}/nginx:${{ steps.tag.outputs.tag }} - fi - - # Build and push Worker image - if [ -f docker/worker/Dockerfile ]; then - docker build -t ${{ env.REGISTRY_URL }}/${{ env.IMAGE_NAME }}/worker:${{ steps.tag.outputs.tag }} -f docker/worker/Dockerfile . - docker push ${{ env.REGISTRY_URL }}/${{ env.IMAGE_NAME }}/worker:${{ steps.tag.outputs.tag }} - fi diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml deleted file mode 100644 index c62e84cc..00000000 --- a/.gitea/workflows/ci.yml +++ /dev/null @@ -1,51 +0,0 @@ -name: CI - -on: - push: - pull_request: - -jobs: - quality: - name: PHP Quality Checks - runs-on: ubuntu-latest - container: - image: composer:2 - steps: - # Gitea runners usually have the repo checked out already. We avoid marketplace actions. - - name: Show versions - run: | - php -v - composer --version - - name: Install dependencies - run: | - composer install --no-interaction --prefer-dist --no-progress - - name: Code style (non-blocking for initial adoption) - run: | - if composer run -q cs 2>/dev/null; then - composer cs || true - else - echo "No 'cs' script found, skipping." - fi - - name: PHPStan - run: | - if composer run -q phpstan 2>/dev/null; then - composer phpstan - else - echo "No 'phpstan' script found, attempting vendor bin..." - ./vendor/bin/phpstan analyse || exit 1 - fi - - name: Pest tests - env: - XDEBUG_MODE: off - run: | - if [ -x ./vendor/bin/pest ]; then - ./vendor/bin/pest -q - elif [ -x ./vendor/bin/phpunit ]; then - ./vendor/bin/phpunit - else - echo "No test runner found." - exit 1 - fi - - name: Composer audit (non-blocking initially) - run: | - composer audit --no-interaction || true diff --git a/.gitea/workflows/update-production-secrets.yml b/.gitea/workflows/update-production-secrets.yml new file mode 100644 index 00000000..94bf18b9 --- /dev/null +++ b/.gitea/workflows/update-production-secrets.yml @@ -0,0 +1,74 @@ +name: Update Production Secrets + +on: + workflow_dispatch: + inputs: + vault_password: + description: 'Ansible Vault Password' + required: true + type: password + +env: + DEPLOYMENT_HOST: 94.16.110.151 + +jobs: + deploy-secrets: + name: Deploy Secrets to Production + runs-on: ubuntu-latest + environment: + name: production-secrets + url: https://michaelschiemer.de + + steps: + - name: Checkout deployment configuration + uses: actions/checkout@v4 + with: + sparse-checkout: | + deployment/ansible + sparse-checkout-cone-mode: false + + - name: Setup SSH key + run: | + mkdir -p ~/.ssh + echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/production + chmod 600 ~/.ssh/production + ssh-keyscan -H ${{ env.DEPLOYMENT_HOST }} >> ~/.ssh/known_hosts + + - name: Install Ansible + run: | + sudo apt-get update + sudo apt-get install -y ansible + + - name: Create vault password file + run: | + echo "${{ github.event.inputs.vault_password }}" > /tmp/.vault_pass + chmod 600 /tmp/.vault_pass + + - name: Deploy secrets via Ansible + run: | + cd deployment/ansible + ansible-playbook -i inventory/production.yml \ + playbooks/setup-production-secrets.yml \ + --vault-password-file /tmp/.vault_pass + + - name: Cleanup vault password + if: always() + run: | + rm -f /tmp/.vault_pass + + - name: Verify secrets deployment + run: | + ssh -i ~/.ssh/production deploy@${{ env.DEPLOYMENT_HOST }} \ + "docker secret ls && test -f /home/deploy/secrets/.env.production" + + - name: Notify deployment success + if: success() + run: | + echo "✅ Secrets deployed successfully to production" + echo "Services will be restarted automatically" + + - name: Notify deployment failure + if: failure() + run: | + echo "❌ Secrets deployment failed" + echo "Check Ansible logs for details" diff --git a/deployment/ansible/playbooks/deploy-update.yml b/deployment/ansible/playbooks/deploy-update.yml index a1b89000..b033e86d 100644 --- a/deployment/ansible/playbooks/deploy-update.yml +++ b/deployment/ansible/playbooks/deploy-update.yml @@ -1,14 +1,15 @@ --- -- name: Deploy Application Update +- name: Deploy Application Update via Docker Compose hosts: production gather_facts: yes - become: yes + become: no vars: # These should be passed via -e from CI/CD image_tag: "{{ image_tag | default('latest') }}" git_commit_sha: "{{ git_commit_sha | default('unknown') }}" deployment_timestamp: "{{ deployment_timestamp | default(ansible_date_time.iso8601) }}" + app_stack_path: "{{ deploy_user_home }}/deployment/stacks/application" pre_tasks: - name: Optionally load registry credentials from encrypted vault @@ -29,12 +30,21 @@ name: docker state: started register: docker_service + become: yes - name: Fail if Docker is not running fail: msg: "Docker service is not running" when: docker_service.status.ActiveState != 'active' + - name: Ensure application stack directory exists + file: + path: "{{ app_stack_path }}" + state: directory + owner: "{{ ansible_user }}" + group: "{{ ansible_user }}" + mode: '0755' + - name: Create backup directory file: path: "{{ backups_path }}/{{ deployment_timestamp | regex_replace(':', '-') }}" @@ -46,16 +56,15 @@ tasks: - name: Backup current deployment metadata shell: | - docker service inspect {{ stack_name }}_app --format '{{ "{{" }}.Spec.TaskTemplate.ContainerSpec.Image{{ "}}" }}' \ - > {{ backups_path }}/{{ deployment_timestamp | regex_replace(':', '-') }}/current_image.txt || true - docker stack ps {{ stack_name }} --format 'table {{ "{{" }}.Name{{ "}}" }}\t{{ "{{" }}.Image{{ "}}" }}\t{{ "{{" }}.CurrentState{{ "}}" }}' \ - > {{ backups_path }}/{{ deployment_timestamp | regex_replace(':', '-') }}/stack_status.txt || true + docker compose -f {{ app_stack_path }}/docker-compose.yml ps --format json 2>/dev/null > {{ backups_path }}/{{ deployment_timestamp | regex_replace(':', '-') }}/current_containers.json || true + docker compose -f {{ app_stack_path }}/docker-compose.yml config 2>/dev/null > {{ backups_path }}/{{ deployment_timestamp | regex_replace(':', '-') }}/docker-compose-config.yml || true args: executable: /bin/bash changed_when: false + ignore_errors: yes - name: Login to Docker registry (if credentials provided) - docker_login: + community.docker.docker_login: registry_url: "{{ docker_registry_url }}" username: "{{ docker_registry_username }}" password: "{{ docker_registry_password }}" @@ -65,43 +74,55 @@ - docker_registry_password is defined - name: Pull new Docker image - docker_image: + community.docker.docker_image: name: "{{ app_image }}" tag: "{{ image_tag }}" source: pull force_source: yes register: image_pull - - name: Update docker-compose.prod.yml with new image tag - lineinfile: - path: "{{ compose_file }}" + - name: Verify image was pulled successfully + fail: + msg: "Failed to pull image {{ app_image }}:{{ image_tag }}" + when: image_pull.failed + + - name: Update docker-compose.yml with new image tag (all services) + replace: + path: "{{ app_stack_path }}/docker-compose.yml" regexp: '^(\s+image:\s+){{ app_image }}:.*$' - line: '\1{{ app_image }}:{{ image_tag }}' - backrefs: yes - when: compose_file is file + replace: '\1{{ app_image }}:{{ image_tag }}' + when: image_pull.changed or image_tag != 'latest' + register: compose_updated - - name: Deploy stack update - docker_stack: - name: "{{ stack_name }}" - compose: - - "{{ compose_file }}" + - name: Restart application stack with new image + community.docker.docker_compose_v2: + project_src: "{{ app_stack_path }}" state: present - prune: yes + pull: always + recreate: always + remove_orphans: yes register: stack_deploy + when: image_pull.changed or compose_updated.changed - - name: Wait for service to be updated - command: > - docker service ps {{ stack_name }}_app - --filter "desired-state=running" - --format '{{ "{{" }}.CurrentState{{ "}}" }}' - register: service_status - until: "'Running' in service_status.stdout" - retries: 30 - delay: 10 + - name: Wait for services to be healthy + wait_for: + timeout: 60 changed_when: false - - name: Get updated service info - command: docker service inspect {{ stack_name }}_app --format '{{ "{{" }}.Spec.TaskTemplate.ContainerSpec.Image{{ "}}" }}' + - name: Check container health status + shell: | + docker compose -f {{ app_stack_path }}/docker-compose.yml ps --format json | jq -r '.[] | select(.Health != "healthy" and .Health != "") | "\(.Name): \(.Health)"' || echo "All healthy or no health checks" + args: + executable: /bin/bash + register: health_status + changed_when: false + ignore_errors: yes + + ?? - name: Get deployed image information + shell: | + docker compose -f {{ app_stack_path }}/docker-compose.yml config | grep -E "^\s+image:" | head -1 | awk '{print $2}' || echo "unknown" + args: + executable: /bin/bash register: deployed_image changed_when: false @@ -112,7 +133,9 @@ Git Commit: {{ git_commit_sha }} Image Tag: {{ image_tag }} Deployed Image: {{ deployed_image.stdout }} - Stack Deploy Output: {{ stack_deploy }} + Image Pull: {{ 'SUCCESS' if image_pull.changed else 'SKIPPED (already exists)' }} + Stack Deploy: {{ 'UPDATED' if stack_deploy.changed else 'NO_CHANGE' }} + Health Status: {{ health_status.stdout if health_status.stdout != '' else 'All services healthy' }} dest: "{{ backups_path }}/{{ deployment_timestamp | regex_replace(':', '-') }}/deployment_metadata.txt" owner: "{{ ansible_user }}" group: "{{ ansible_user }}" @@ -121,17 +144,22 @@ - name: Cleanup old backups (keep last {{ max_rollback_versions }}) shell: | cd {{ backups_path }} - ls -t | tail -n +{{ max_rollback_versions + 1 }} | xargs -r rm -rf + ls -dt */ 2>/dev/null | tail -n +{{ max_rollback_versions + 1 }} | xargs -r rm -rf args: executable: /bin/bash changed_when: false + ignore_errors: yes post_tasks: - name: Display deployment summary debug: msg: - - "Deployment completed successfully!" + - "=== Deployment Summary ===" - "Image: {{ app_image }}:{{ image_tag }}" - "Commit: {{ git_commit_sha }}" - "Timestamp: {{ deployment_timestamp }}" - - "Health check URL: {{ health_check_url }}" + - "Image Pull: {{ 'SUCCESS' if image_pull.changed else 'SKIPPED' }}" + - "Stack Deploy: {{ 'UPDATED' if stack_deploy.changed else 'NO_CHANGE' }}" + - "Health Check URL: {{ health_check_url }}" + - "" + - "Next: Verify application is healthy" diff --git a/deployment/ansible/playbooks/rollback.yml b/deployment/ansible/playbooks/rollback.yml new file mode 100644 index 00000000..7dd2bad8 --- /dev/null +++ b/deployment/ansible/playbooks/rollback.yml @@ -0,0 +1,166 @@ +--- +- name: Rollback Application Deployment + hosts: production + gather_facts: yes + become: no + + vars: + rollback_to_version: "{{ rollback_to_version | default('previous') }}" + app_stack_path: "{{ deploy_user_home }}/deployment/stacks/application" + + pre_tasks: + - name: Optionally load registry credentials from encrypted vault + include_vars: + file: "{{ playbook_dir }}/../secrets/production.vault.yml" + no_log: yes + ignore_errors: yes + delegate_to: localhost + become: no + + - name: Derive docker registry credentials from vault when not provided + set_fact: + docker_registry_username: "{{ docker_registry_username | default(vault_docker_registry_username | default(omit)) }}" + docker_registry_password: "{{ docker_registry_password | default(vault_docker_registry_password | default(omit)) }}" + + - name: Check Docker service + systemd: + name: docker + state: started + register: docker_service + become: yes + + - name: Fail if Docker is not running + fail: + msg: "Docker service is not running - cannot perform rollback" + when: docker_service.status.ActiveState != 'active' + + - name: Get list of available backups + find: + paths: "{{ backups_path }}" + file_type: directory + register: available_backups + + - name: Fail if no backups available + fail: + msg: "No backup versions available for rollback" + when: available_backups.matched == 0 + + - name: Sort backups by date (newest first) + set_fact: + sorted_backups: "{{ available_backups.files | sort(attribute='mtime', reverse=true) }}" + + tasks: + - name: Determine rollback target + set_fact: + rollback_backup: "{{ sorted_backups[1] if rollback_to_version == 'previous' else sorted_backups | selectattr('path', 'search', rollback_to_version) | first }}" + when: sorted_backups | length > 1 + + - name: Fail if rollback target not found + fail: + msg: "Cannot determine rollback target. Available backups: {{ sorted_backups | map(attribute='path') | list }}" + when: rollback_backup is not defined + + - name: Load rollback metadata + slurp: + src: "{{ rollback_backup.path }}/deployment_metadata.txt" + register: rollback_metadata + ignore_errors: yes + + - name: Parse rollback image from metadata + set_fact: + rollback_image: "{{ rollback_metadata.content | b64decode | regex_search('Deployed Image: ([^\\n]+)', '\\1') | first }}" + when: rollback_metadata is succeeded + + - name: Alternative: Parse rollback image from docker-compose config backup + set_fact: + rollback_image: "{{ rollback_metadata.content | b64decode | regex_search('image:\\s+([^:]+):([^\\n]+)', '\\1:\\2') | first }}" + when: + - rollback_metadata is succeeded + - rollback_image is not defined + + - name: Fail if cannot determine rollback image + fail: + msg: "Cannot determine image to rollback to from backup: {{ rollback_backup.path }}" + when: rollback_image is not defined or rollback_image == '' + + - name: Display rollback information + debug: + msg: + - "Rolling back to previous version" + - "Backup: {{ rollback_backup.path }}" + - "Image: {{ rollback_image }}" + + - name: Login to Docker registry (if credentials provided) + community.docker.docker_login: + registry_url: "{{ docker_registry_url }}" + username: "{{ docker_registry_username }}" + password: "{{ docker_registry_password }}" + no_log: yes + when: + - docker_registry_username is defined + - docker_registry_password is defined + + - name: Pull rollback image + community.docker.docker_image: + name: "{{ rollback_image.split(':')[0] }}" + tag: "{{ rollback_image.split(':')[1] }}" + source: pull + force_source: yes + register: image_pull + + - name: Update docker-compose.yml with rollback image + replace: + path: "{{ app_stack_path }}/docker-compose.yml" + regexp: '^(\s+image:\s+){{ app_image }}:.*$' + replace: '\1{{ rollback_image }}' + register: compose_updated + + - name: Restart application stack with rollback image + community.docker.docker_compose_v2: + project_src: "{{ app_stack_path }}" + state: present + pull: always + recreate: always + remove_orphans: yes + register: stack_rollback + when: compose_updated.changed + + - name: Wait for services to be healthy after rollback + wait_for: + timeout: 60 + changed_when: false + + - name: Get current running image + shell: | + docker compose -f {{ app_stack_path }}/docker-compose.yml config | grep -E "^\s+image:" | head -1 | awk '{print $2}' || echo "unknown" + args: + executable: /bin/bash + register: current_image + changed_when: false + + - name: Record rollback event + copy: + content: | + Rollback Timestamp: {{ ansible_date_time.iso8601 }} + Rolled back from: {{ sorted_backups[0].path | basename }} + Rolled back to: {{ rollback_backup.path | basename }} + Rollback Image: {{ rollback_image }} + Current Image: {{ current_image.stdout }} + dest: "{{ backups_path }}/rollback_{{ ansible_date_time.epoch }}.txt" + owner: "{{ ansible_user }}" + group: "{{ ansible_user }}" + mode: '0644' + + post_tasks: + - name: Display rollback summary + debug: + msg: + - "✅ Rollback completed successfully!" + - "Rolled back to: {{ rollback_image }}" + - "From backup: {{ rollback_backup.path }}" + - "Current running image: {{ current_image.stdout }}" + - "Health check URL: {{ health_check_url }}" + + - name: Recommend health check + debug: + msg: "⚠️ Please verify application health at {{ health_check_url }}" diff --git a/deployment/ansible/playbooks/setup-infrastructure.yml b/deployment/ansible/playbooks/setup-infrastructure.yml new file mode 100644 index 00000000..cb8f419f --- /dev/null +++ b/deployment/ansible/playbooks/setup-infrastructure.yml @@ -0,0 +1,210 @@ +--- +- name: Deploy Infrastructure Stacks on Production Server + hosts: production + become: no + gather_facts: yes + + vars: + stacks_base_path: "~/deployment/stacks" + wait_timeout: 60 + + tasks: + - name: Check if deployment stacks directory exists + stat: + path: "{{ stacks_base_path }}" + register: stacks_dir + + - name: Fail if stacks directory doesn't exist + fail: + msg: "Deployment stacks directory not found at {{ stacks_base_path }}" + when: not stacks_dir.stat.exists + + # Create external network required by all stacks + - name: Create traefik-public network + community.docker.docker_network: + name: traefik-public + driver: bridge + state: present + + # 1. Deploy Traefik (Reverse Proxy & SSL) + - name: Deploy Traefik stack + community.docker.docker_compose_v2: + project_src: "{{ stacks_base_path }}/traefik" + state: present + pull: always + register: traefik_output + + - name: Wait for Traefik to be ready + wait_for: + timeout: "{{ wait_timeout }}" + when: traefik_output.changed + + - name: Check Traefik logs for readiness hint + shell: docker compose logs traefik 2>&1 | grep -Ei "(configuration loaded|traefik[[:space:]]v|Starting provider|Server configuration loaded)" || true + args: + chdir: "{{ stacks_base_path }}/traefik" + register: traefik_logs + until: traefik_logs.stdout != "" + retries: 6 + delay: 10 + changed_when: false + ignore_errors: yes + + # 2. Deploy PostgreSQL (Database) + - name: Deploy PostgreSQL stack + community.docker.docker_compose_v2: + project_src: "{{ stacks_base_path }}/postgresql" + state: present + pull: always + register: postgres_output + + - name: Wait for PostgreSQL to be ready + wait_for: + timeout: "{{ wait_timeout }}" + when: postgres_output.changed + + - name: Check PostgreSQL logs for readiness + shell: docker compose logs postgres 2>&1 | grep -Ei "(ready to accept connections|database system is ready)" || true + args: + chdir: "{{ stacks_base_path }}/postgresql" + register: postgres_logs + until: postgres_logs.stdout != "" + retries: 6 + delay: 10 + changed_when: false + ignore_errors: yes + + # 3. Deploy Docker Registry (Private Registry) + - name: Ensure Registry auth directory exists + file: + path: "{{ stacks_base_path }}/registry/auth" + state: directory + mode: '0755' + become: yes + + - name: Create Registry htpasswd file if missing + shell: | + if [ ! -f {{ stacks_base_path }}/registry/auth/htpasswd ]; then + docker run --rm --entrypoint htpasswd httpd:2 -Bbn admin registry-secure-password-2025 > {{ stacks_base_path }}/registry/auth/htpasswd + chmod 644 {{ stacks_base_path }}/registry/auth/htpasswd + fi + args: + executable: /bin/bash + become: yes + changed_when: true + register: registry_auth_created + + - name: Deploy Docker Registry stack + community.docker.docker_compose_v2: + project_src: "{{ stacks_base_path }}/registry" + state: present + pull: always + register: registry_output + + - name: Wait for Docker Registry to be ready + wait_for: + timeout: "{{ wait_timeout }}" + when: registry_output.changed + + - name: Check Registry logs for readiness + shell: docker compose logs registry 2>&1 | grep -Ei "(listening on|listening at|http server)" || true + args: + chdir: "{{ stacks_base_path }}/registry" + register: registry_logs + until: registry_logs.stdout != "" + retries: 6 + delay: 10 + changed_when: false + ignore_errors: yes + + - name: Verify Registry is accessible + uri: + url: "http://127.0.0.1:5000/v2/_catalog" + user: admin + password: registry-secure-password-2025 + status_code: 200 + timeout: 5 + register: registry_check + ignore_errors: yes + changed_when: false + + - name: Display Registry status + debug: + msg: "Registry accessibility: {{ 'SUCCESS' if registry_check.status == 200 else 'FAILED - may need manual check' }}" + + # 4. Deploy Gitea (CRITICAL - Git Server + MySQL + Redis) + - name: Deploy Gitea stack + community.docker.docker_compose_v2: + project_src: "{{ stacks_base_path }}/gitea" + state: present + pull: always + register: gitea_output + + - name: Wait for Gitea to be ready + wait_for: + timeout: "{{ wait_timeout }}" + when: gitea_output.changed + + - name: Check Gitea logs for readiness + shell: docker compose logs gitea 2>&1 | grep -Ei "(Listen:|Server is running|Starting server)" || true + args: + chdir: "{{ stacks_base_path }}/gitea" + register: gitea_logs + until: gitea_logs.stdout != "" + retries: 12 + delay: 10 + changed_when: false + ignore_errors: yes + + # 5. Deploy Monitoring (Portainer + Grafana + Prometheus) + - name: Deploy Monitoring stack + community.docker.docker_compose_v2: + project_src: "{{ stacks_base_path }}/monitoring" + state: present + pull: always + register: monitoring_output + + - name: Wait for Monitoring to be ready + wait_for: + timeout: "{{ wait_timeout }}" + when: monitoring_output.changed + + # Verification + - name: List all running containers + command: > + docker ps --format 'table {{ "{{" }}.Names{{ "}}" }}\t{{ "{{" }}.Status{{ "}}" }}\t{{ "{{" }}.Ports{{ "}}" }}' + register: docker_ps_output + + - name: Display running containers + debug: + msg: "{{ docker_ps_output.stdout_lines }}" + + - name: Verify Gitea accessibility via HTTPS + uri: + url: https://git.michaelschiemer.de + method: GET + validate_certs: no + status_code: 200 + timeout: 10 + register: gitea_http_check + ignore_errors: yes + + - name: Display Gitea accessibility status + debug: + msg: "Gitea HTTPS check: {{ 'SUCCESS' if gitea_http_check.status == 200 else 'FAILED - Status: ' + (gitea_http_check.status|string) }}" + + - name: Summary + debug: + msg: + - "=== Infrastructure Deployment Complete ===" + - "Traefik: {{ 'Deployed' if traefik_output.changed else 'Already running' }}" + - "PostgreSQL: {{ 'Deployed' if postgres_output.changed else 'Already running' }}" + - "Docker Registry: {{ 'Deployed' if registry_output.changed else 'Already running' }}" + - "Gitea: {{ 'Deployed' if gitea_output.changed else 'Already running' }}" + - "Monitoring: {{ 'Deployed' if monitoring_output.changed else 'Already running' }}" + - "" + - "Next Steps:" + - "1. Access Gitea at: https://git.michaelschiemer.de" + - "2. Complete Gitea setup wizard if first-time deployment" + - "3. Navigate to Admin > Actions > Runners to get registration token" + - "4. Continue with Phase 1 - Gitea Runner Setup" diff --git a/deployment/ansible/playbooks/setup-production-secrets.yml b/deployment/ansible/playbooks/setup-production-secrets.yml new file mode 100644 index 00000000..e961de5c --- /dev/null +++ b/deployment/ansible/playbooks/setup-production-secrets.yml @@ -0,0 +1,92 @@ +--- +- name: Setup Production Secrets + hosts: production + gather_facts: yes + become: yes + + vars: + vault_file: "{{ playbook_dir }}/../secrets/production.vault.yml" + + pre_tasks: + - name: Verify vault file exists + stat: + path: "{{ vault_file }}" + register: vault_stat + delegate_to: localhost + become: no + + - name: Fail if vault file missing + fail: + msg: "Vault file not found at {{ vault_file }}" + when: not vault_stat.stat.exists + + tasks: + - name: Detect Docker Swarm mode + shell: docker info -f '{{ "{{" }}.Swarm.LocalNodeState{{ "}}" }}' + register: swarm_state + changed_when: false + + - name: Set fact if swarm is active + set_fact: + swarm_active: "{{ swarm_state.stdout | lower == 'active' }}" + + - name: Load encrypted secrets + include_vars: + file: "{{ vault_file }}" + no_log: yes + + - name: Ensure secrets directory exists + file: + path: "{{ secrets_path }}" + state: directory + owner: "{{ ansible_user }}" + group: "{{ ansible_user }}" + mode: '0700' + + - name: Create .env.production file + template: + src: "{{ playbook_dir }}/../templates/.env.production.j2" + dest: "{{ secrets_path }}/.env.production" + owner: "{{ ansible_user }}" + group: "{{ ansible_user }}" + mode: '0600' + no_log: yes + + - name: Create Docker secrets from vault (disabled for compose-only deployment) + docker_secret: + name: "{{ item.name }}" + data: "{{ item.value }}" + state: present + loop: + - name: db_password + value: "{{ vault_db_password }}" + - name: redis_password + value: "{{ vault_redis_password }}" + - name: app_key + value: "{{ vault_app_key }}" + - name: jwt_secret + value: "{{ vault_jwt_secret }}" + - name: mail_password + value: "{{ vault_mail_password }}" + no_log: yes + when: false + + - name: Set secure permissions on secrets directory + file: + path: "{{ secrets_path }}" + state: directory + owner: "{{ ansible_user }}" + group: "{{ ansible_user }}" + mode: '0700' + recurse: yes + + - name: Verify Docker secrets (skipped) + command: docker secret ls --format '{{ "{{" }}.Name{{ "}}" }}' + register: docker_secrets + changed_when: false + when: false + + - name: Display deployed Docker secrets (skipped) + debug: + msg: "Deployed secrets: {{ docker_secrets.stdout_lines | default([]) }}" + when: false diff --git a/deployment/ansible/playbooks/setup-ssl-certificates.yml b/deployment/ansible/playbooks/setup-ssl-certificates.yml new file mode 100644 index 00000000..621606da --- /dev/null +++ b/deployment/ansible/playbooks/setup-ssl-certificates.yml @@ -0,0 +1,86 @@ +--- +- name: Setup Let's Encrypt SSL Certificates via Traefik + hosts: production + become: no + gather_facts: yes + + vars: + domains: + - git.michaelschiemer.de + - michaelschiemer.de + acme_email: kontakt@michaelschiemer.de + + tasks: + - name: Check if acme.json exists and is a file + stat: + path: "{{ deploy_user_home }}/de iployment/stacks/traefik/acme.json" + register: acme_stat + + - name: Remove acme.json if it's a directory + file: + path: "{{ deploy_user_home }}/deployment/stacks/traefik/acme.json" + state: absent + become: yes + when: acme_stat.stat.exists and acme_stat.stat.isdir + + - name: Ensure Traefik acme.json exists and has correct permissions + file: + path: "{{ deploy_user_home }}/deployment/stacks/traefik/acme.json" + state: touch + mode: '0600' + owner: "{{ ansible_user }}" + group: "{{ ansible_user }}" + become: yes + + - name: Verify Traefik is running + command: docker compose -f {{ deploy_user_home }}/deployment/stacks/traefik/docker-compose.yml ps traefik + register: traefik_status + changed_when: false + + - name: Fail if Traefik is not running + fail: + msg: "Traefik is not running. Please start it first." + when: traefik_status.rc != 0 or "Up" not in traefik_status.stdout + + - name: Force Traefik to reload configuration + command: docker compose -f {{ deploy_user_home }}/deployment/stacks/traefik/docker-compose.yml restart traefik + changed_when: true + + - name: Wait for Traefik to be ready + wait_for: + timeout: 10 + changed_when: false + + - name: Trigger certificate request by accessing each domain + uri: + url: "https://{{ item }}" + method: GET + validate_certs: no + timeout: 5 + status_code: [200, 301, 302, 303, 404, 502, 503] + loop: "{{ domains }}" + register: certificate_trigger + changed_when: false + ignore_errors: yes + + - name: Wait for ACME certificate generation (30 seconds) + wait_for: + timeout: 30 + changed_when: false + + - name: Check if acme.json contains certificates + stat: + path: "{{ deploy_user_home }}/deployment/stacks/traefik/acme.json" + register: acme_file + + - name: Display certificate status + debug: + msg: | + Certificate setup triggered. + Traefik will request Let's Encrypt certificates for: + {{ domains | join(', ') }} + + Check Traefik logs to see certificate generation progress: + docker compose -f {{ deploy_user_home }}/deployment/stacks/traefik/docker-compose.yml logs traefik | grep -i acme + + Certificates should be ready within 1-2 minutes. diff --git a/deployment/ansible/playbooks/sync-stacks.yml b/deployment/ansible/playbooks/sync-stacks.yml new file mode 100644 index 00000000..0b924164 --- /dev/null +++ b/deployment/ansible/playbooks/sync-stacks.yml @@ -0,0 +1,53 @@ +--- +- name: Sync Infrastructure Stacks to Production Server + hosts: production + become: no + gather_facts: yes + + vars: + local_stacks_path: "/home/michael/dev/michaelschiemer/deployment/stacks" + remote_stacks_path: "~/deployment" + + tasks: + - name: Ensure deployment directory exists on production + file: + path: "{{ remote_stacks_path }}" + state: directory + mode: '0755' + + - name: Sync stacks directory to production server + synchronize: + src: "{{ local_stacks_path }}" + dest: "{{ remote_stacks_path }}/" + delete: no + recursive: yes + rsync_opts: + - "--chmod=D755,F644" + - "--exclude=.git" + - "--exclude=*.log" + - "--exclude=data/" + - "--exclude=volumes/" + + - name: Ensure executable permissions on PostgreSQL backup scripts + file: + path: "{{ item }}" + mode: '0755' + loop: + - "{{ remote_stacks_path }}/stacks/postgresql/scripts/backup-entrypoint.sh" + - "{{ remote_stacks_path }}/stacks/postgresql/scripts/backup.sh" + - "{{ remote_stacks_path }}/stacks/postgresql/scripts/restore.sh" + ignore_errors: yes + + - name: Verify stacks directory exists on production + stat: + path: "{{ remote_stacks_path }}/stacks" + register: stacks_dir + + - name: Display sync results + debug: + msg: + - "=== Stacks Synchronization Complete ===" + - "Stacks directory exists: {{ stacks_dir.stat.exists }}" + - "Path: {{ remote_stacks_path }}/stacks" + - "" + - "Next: Run infrastructure deployment playbook" diff --git a/scripts/deploy-production.sh b/scripts/deploy-production.sh deleted file mode 100755 index 5a4fca09..00000000 --- a/scripts/deploy-production.sh +++ /dev/null @@ -1,83 +0,0 @@ -#!/bin/bash - -# Production Deployment Script -# This script prepares the application for production deployment - -set -e - -echo "🚀 Starting Production Deployment..." - -# Check if we're in the right directory -if [ ! -f "composer.json" ]; then - echo "❌ Error: Must be run from project root directory" - exit 1 -fi - -# Backup current .env if it exists -if [ -f ".env" ]; then - echo "📦 Backing up current .env to .env.backup" - cp .env .env.backup -fi - -# Copy production environment file -echo "📝 Setting up production environment..." -cp .env.production .env - -# Clear all caches -echo "🧹 Clearing caches..." -rm -rf storage/cache/* -rm -rf var/cache/* -rm -rf cache/* - -# Install production dependencies (no dev dependencies) -echo "📦 Installing production dependencies..." -composer install --no-dev --optimize-autoloader --no-interaction - -# Build production assets -echo "🎨 Building production assets..." -npm run build - -# Set correct permissions -echo "🔐 Setting correct permissions..." -chmod -R 755 storage/ -chmod -R 755 var/ -chmod -R 755 public/ - -# Create necessary directories -mkdir -p storage/logs -mkdir -p storage/cache -mkdir -p var/cache -mkdir -p var/logs - -# Run database migrations -echo "🗄️ Running database migrations..." -php console.php db:migrate --force - -# Clear PHP opcache if available -if command -v cachetool &> /dev/null; then - echo "🔄 Clearing PHP opcache..." - cachetool opcache:reset -fi - -# Restart services (if using systemctl) -if command -v systemctl &> /dev/null; then - echo "🔄 Restarting services..." - sudo systemctl restart php8.4-fpm - sudo systemctl restart nginx -fi - -echo "✅ Production deployment complete!" -echo "" -echo "⚠️ IMPORTANT REMINDERS:" -echo "1. Ensure APP_ENV=production in .env" -echo "2. Ensure APP_DEBUG=false in .env" -echo "3. Update database credentials if needed" -echo "4. Update ADMIN_ALLOWED_IPS in .env for admin access" -echo "5. Test the site to ensure everything works" -echo "" -echo "🔒 Security Checklist:" -echo "[ ] Performance debug is disabled" -echo "[ ] Session debug info is hidden" -echo "[ ] Admin routes are IP-restricted" -echo "[ ] Error messages are generic" -echo "[ ] HTTPS is enforced" \ No newline at end of file diff --git a/scripts/maintenance/populate_images_from_filesystem.php b/scripts/maintenance/populate_images_from_filesystem.php index 75b0dbfc..a839cf0e 100644 --- a/scripts/maintenance/populate_images_from_filesystem.php +++ b/scripts/maintenance/populate_images_from_filesystem.php @@ -221,4 +221,4 @@ try { exit(1); } -echo "\n🎉 Migration script completed!\n"; \ No newline at end of file +echo "\n🎉 Migration script completed!\n"; diff --git a/scripts/prepare-secrets.sh b/scripts/prepare-secrets.sh new file mode 100755 index 00000000..975dc8ba --- /dev/null +++ b/scripts/prepare-secrets.sh @@ -0,0 +1,34 @@ +#!/bin/bash +# Helper Script to prepare secrets for Gitea manual setup +# Usage: ./scripts/prepare-secrets.sh + +set -euo pipefail + +echo "=== Gitea Secrets - Werte zum Kopieren ===" +echo "" +echo "1. REGISTRY_USER:" +echo "─────────────────" +echo "admin" +echo "" +echo "" +echo "2. REGISTRY_PASSWORD:" +echo "─────────────────────" +echo "registry-secure-password-2025" +echo "" +echo "" +echo "3. SSH_PRIVATE_KEY:" +echo "───────────────────" +if [ -f ~/.ssh/production ]; then + cat ~/.ssh/production +else + echo "⚠️ FEHLER: ~/.ssh/production nicht gefunden!" + echo "Bitte SSH Key erstellen oder Pfad anpassen." + exit 1 +fi +echo "" +echo "" +echo "=== Nächste Schritte ===" +echo "1. Gehe zu: https://git.michaelschiemer.de//michaelschiemer/settings/secrets" +echo "2. Füge jedes Secret oben einzeln hinzu" +echo "3. Kopiere die Werte von oben für jedes Secret" +echo "" diff --git a/scripts/production-deploy.sh b/scripts/production-deploy.sh deleted file mode 100755 index b8ccad9e..00000000 --- a/scripts/production-deploy.sh +++ /dev/null @@ -1,446 +0,0 @@ -#!/bin/bash - -# Production Deployment Script for Custom PHP Framework -# Comprehensive deployment automation with zero-downtime strategy -# -# Usage: -# ./scripts/production-deploy.sh [initial|update|rollback] -# -# Modes: -# initial - First-time production deployment -# update - Rolling update with zero downtime -# rollback - Rollback to previous version - -set -euo pipefail - -# Configuration -DEPLOY_MODE="${1:-update}" -PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -BACKUP_DIR="${PROJECT_ROOT}/../backups" -TIMESTAMP=$(date +%Y%m%d_%H%M%S) -BACKUP_PATH="${BACKUP_DIR}/backup_${TIMESTAMP}" - -# Colors -GREEN="\e[32m" -YELLOW="\e[33m" -RED="\e[31m" -BLUE="\e[34m" -RESET="\e[0m" - -# Logging functions -log() { - echo -e "${BLUE}[$(date +'%H:%M:%S')]${RESET} $1" -} - -success() { - echo -e "${GREEN}✅ $1${RESET}" -} - -warning() { - echo -e "${YELLOW}⚠️ $1${RESET}" -} - -error() { - echo -e "${RED}❌ $1${RESET}" - cleanup_on_error - exit 1 -} - -# Cleanup on error -cleanup_on_error() { - log "Cleaning up after error..." - - if [[ -d "$BACKUP_PATH" ]]; then - warning "Rolling back to previous version..." - restore_backup "$BACKUP_PATH" - fi -} - -# Prerequisites check -check_prerequisites() { - log "Checking prerequisites..." - - # Check if running from project root - if [[ ! -f "$PROJECT_ROOT/composer.json" ]]; then - error "Must be run from project root directory" - fi - - # Check Docker - if ! command -v docker &> /dev/null; then - error "Docker is not installed" - fi - - # Check Docker Compose - if ! docker compose version &> /dev/null; then - error "Docker Compose is not installed" - fi - - # Check .env.production exists - if [[ ! -f "$PROJECT_ROOT/.env.production" ]]; then - error ".env.production not found - copy from .env.example and configure" - fi - - # Check docker-compose.production.yml exists - if [[ ! -f "$PROJECT_ROOT/docker-compose.production.yml" ]]; then - error "docker-compose.production.yml not found" - fi - - # Verify VAULT_ENCRYPTION_KEY is set - if ! grep -q "VAULT_ENCRYPTION_KEY=" "$PROJECT_ROOT/.env.production" || \ - grep -q "VAULT_ENCRYPTION_KEY=CHANGE_ME" "$PROJECT_ROOT/.env.production"; then - error "VAULT_ENCRYPTION_KEY not configured in .env.production" - fi - - success "Prerequisites check passed" -} - -# Create backup -create_backup() { - log "Creating backup..." - - mkdir -p "$BACKUP_DIR" - - # Backup database - if docker compose ps db | grep -q "Up"; then - log "Backing up database..." - docker compose exec -T db pg_dump -U postgres michaelschiemer_prod | \ - gzip > "${BACKUP_PATH}_database.sql.gz" - success "Database backup created" - fi - - # Backup .env - if [[ -f "$PROJECT_ROOT/.env" ]]; then - cp "$PROJECT_ROOT/.env" "${BACKUP_PATH}_env" - success ".env backup created" - fi - - # Backup docker volumes (important directories) - if [[ -d "$PROJECT_ROOT/storage" ]]; then - tar -czf "${BACKUP_PATH}_storage.tar.gz" -C "$PROJECT_ROOT" storage - success "Storage backup created" - fi - - success "Backup completed: $BACKUP_PATH" -} - -# Restore from backup -restore_backup() { - local backup_path="$1" - - log "Restoring from backup: $backup_path" - - # Restore database - if [[ -f "${backup_path}_database.sql.gz" ]]; then - log "Restoring database..." - gunzip -c "${backup_path}_database.sql.gz" | \ - docker compose exec -T db psql -U postgres michaelschiemer_prod - success "Database restored" - fi - - # Restore .env - if [[ -f "${backup_path}_env" ]]; then - cp "${backup_path}_env" "$PROJECT_ROOT/.env" - success ".env restored" - fi - - # Restore storage - if [[ -f "${backup_path}_storage.tar.gz" ]]; then - tar -xzf "${backup_path}_storage.tar.gz" -C "$PROJECT_ROOT" - success "Storage restored" - fi - - # Restart services - docker compose -f docker-compose.yml \ - -f docker-compose.production.yml \ - --env-file .env.production \ - restart - - success "Backup restored successfully" -} - -# Build Docker images -build_images() { - log "Building Docker images..." - - cd "$PROJECT_ROOT" - - docker compose -f docker-compose.yml \ - -f docker-compose.production.yml \ - --env-file .env.production \ - build --no-cache - - success "Docker images built" -} - -# Run database migrations -run_migrations() { - log "Running database migrations..." - - cd "$PROJECT_ROOT" - - # Check migration status first - docker compose exec -T php php console.php db:status || true - - # Run migrations - if ! docker compose exec -T php php console.php db:migrate; then - error "Database migrations failed" - fi - - success "Database migrations completed" -} - -# Initialize SSL certificates -init_ssl() { - log "Initializing SSL certificates..." - - cd "$PROJECT_ROOT" - - # Check if SSL is enabled - if grep -q "SSL_ENABLED=true" .env.production; then - log "SSL is enabled, checking certificate status..." - - # Check certificate status - if docker compose exec -T php php console.php ssl:status 2>/dev/null | grep -q "Certificate is valid"; then - success "SSL certificate already exists and is valid" - else - warning "SSL certificate not found or invalid, initializing..." - - if ! docker compose exec -T php php console.php ssl:init; then - error "SSL initialization failed" - fi - - success "SSL certificate initialized" - fi - else - warning "SSL is disabled in .env.production" - fi -} - -# Verify Vault configuration -verify_vault() { - log "Verifying Vault configuration..." - - cd "$PROJECT_ROOT" - - # Test Vault access - if ! docker compose exec -T php php console.php vault:list &>/dev/null; then - error "Vault not accessible - check VAULT_ENCRYPTION_KEY" - fi - - success "Vault is configured correctly" -} - -# Health check with retries -health_check() { - local max_retries=30 - local retry_count=0 - - log "Running health checks..." - - while [[ $retry_count -lt $max_retries ]]; do - if curl -f -s -k -H "User-Agent: Mozilla/5.0 (Deployment Health Check)" "https://localhost/health" > /dev/null 2>&1; then - success "Health check passed" - return 0 - fi - - retry_count=$((retry_count + 1)) - log "Health check attempt $retry_count/$max_retries..." - sleep 2 - done - - error "Health check failed after $max_retries attempts" -} - -# Initial deployment -initial_deployment() { - log "🚀 Starting initial production deployment..." - - check_prerequisites - - cd "$PROJECT_ROOT" - - # 1. Generate Vault encryption key if not exists - if grep -q "VAULT_ENCRYPTION_KEY=CHANGE_ME" .env.production; then - log "Generating Vault encryption key..." - warning "Make sure to backup this key securely!" - # Key generation is done manually for security - error "Please generate VAULT_ENCRYPTION_KEY with: docker exec php php console.php vault:generate-key" - fi - - # 2. Build images - build_images - - # 3. Start services - log "Starting Docker services..." - docker compose -f docker-compose.yml \ - -f docker-compose.production.yml \ - --env-file .env.production \ - up -d - - # 4. Wait for services to be ready - log "Waiting for services to be ready..." - sleep 20 - - # 5. Run migrations - run_migrations - - # 6. Initialize SSL - init_ssl - - # 7. Verify Vault - verify_vault - - # 8. Health check - health_check - - # 9. Display summary - deployment_summary - - success "🎉 Initial deployment completed successfully!" -} - -# Update deployment (zero-downtime) -update_deployment() { - log "🔄 Starting rolling update deployment..." - - check_prerequisites - create_backup - - cd "$PROJECT_ROOT" - - # 1. Pull latest images (if using registry) - log "Pulling latest images..." - docker compose -f docker-compose.yml \ - -f docker-compose.production.yml \ - --env-file .env.production \ - pull || warning "Pull failed (not critical if building locally)" - - # 2. Build new images - build_images - - # 3. Run migrations (if any) - log "Running database migrations..." - docker compose exec -T php php console.php db:migrate || warning "No new migrations" - - # 4. Rolling restart with health checks - log "Performing rolling restart..." - - # Restart PHP-FPM first - docker compose -f docker-compose.yml \ - -f docker-compose.production.yml \ - --env-file .env.production \ - up -d --no-deps --force-recreate php - - sleep 10 - - # Restart web server - docker compose -f docker-compose.yml \ - -f docker-compose.production.yml \ - --env-file .env.production \ - up -d --no-deps --force-recreate web - - sleep 5 - - # Restart queue workers (graceful shutdown via stop_grace_period) - docker compose -f docker-compose.yml \ - -f docker-compose.production.yml \ - --env-file .env.production \ - up -d --no-deps --force-recreate --scale queue-worker=2 queue-worker - - # 5. Health check - health_check - - # 6. Cleanup old images - log "Cleaning up old Docker images..." - docker image prune -f - - # 7. Display summary - deployment_summary - - success "🎉 Update deployment completed successfully!" -} - -# Rollback deployment -rollback_deployment() { - log "⏪ Starting rollback..." - - # Find latest backup - local latest_backup=$(find "$BACKUP_DIR" -name "backup_*_database.sql.gz" | sort -r | head -1) - - if [[ -z "$latest_backup" ]]; then - error "No backup found for rollback" - fi - - local backup_prefix="${latest_backup%_database.sql.gz}" - - warning "Rolling back to: $backup_prefix" - read -p "Continue? (yes/no): " confirm - - if [[ "$confirm" != "yes" ]]; then - log "Rollback cancelled" - exit 0 - fi - - restore_backup "$backup_prefix" - health_check - - success "🎉 Rollback completed successfully!" -} - -# Deployment summary -deployment_summary() { - echo "" - echo -e "${GREEN}========================================${RESET}" - echo -e "${GREEN} Deployment Summary${RESET}" - echo -e "${GREEN}========================================${RESET}" - echo "" - echo "📋 Mode: $DEPLOY_MODE" - echo "⏰ Timestamp: $(date)" - echo "📁 Project: $PROJECT_ROOT" - echo "💾 Backup: $BACKUP_PATH" - echo "" - echo "🐳 Docker Services:" - docker compose ps - echo "" - echo "🔒 Security Checks:" - echo " [ ] APP_ENV=production in .env.production" - echo " [ ] APP_DEBUG=false in .env.production" - echo " [ ] VAULT_ENCRYPTION_KEY configured" - echo " [ ] ADMIN_ALLOWED_IPS configured" - echo " [ ] SSL certificates valid" - echo "" - echo "📊 Health Check:" - echo " ✅ Application: https://localhost/health" - echo "" - echo "📝 Next Steps:" - echo " 1. Verify all services are running" - echo " 2. Check logs: docker compose logs -f --tail=100" - echo " 3. Test critical user flows" - echo " 4. Monitor error rates" - echo "" - echo -e "${GREEN}========================================${RESET}" -} - -# Main deployment logic -main() { - case "$DEPLOY_MODE" in - initial) - initial_deployment - ;; - update) - update_deployment - ;; - rollback) - rollback_deployment - ;; - *) - error "Invalid deployment mode: $DEPLOY_MODE. Use: initial|update|rollback" - ;; - esac -} - -# Trap errors -trap cleanup_on_error ERR - -# Run main -main "$@" diff --git a/scripts/server-deploy.sh b/scripts/server-deploy.sh deleted file mode 100644 index 6758ee62..00000000 --- a/scripts/server-deploy.sh +++ /dev/null @@ -1,240 +0,0 @@ -#!/bin/bash - -# Server-side Deployment Script für Custom PHP Framework -# Läuft auf dem Production-Server (94.16.110.151) - -set -euo pipefail # Exit on any error - -DEPLOY_TAG="$1" -DEPLOY_PATH="$2" -BACKUP_PATH="${DEPLOY_PATH}-backup-$(date +%Y%m%d-%H%M%S)" -TEMP_PATH="${DEPLOY_PATH}-deploying" - -# Farben -GREEN="\e[32m" -YELLOW="\e[33m" -RED="\e[31m" -BLUE="\e[34m" -RESET="\e[0m" - -log() { - echo -e "${BLUE}[SERVER $(date +'%H:%M:%S')]${RESET} $1" -} - -success() { - echo -e "${GREEN}✅ $1${RESET}" -} - -error() { - echo -e "${RED}❌ $1${RESET}" - cleanup_on_error - exit 1 -} - -cleanup_on_error() { - log "Cleanup nach Fehler..." - [[ -d "$TEMP_PATH" ]] && rm -rf "$TEMP_PATH" - [[ -d "$BACKUP_PATH" ]] && { - log "Stelle vorherige Version wieder her..." - [[ -d "$DEPLOY_PATH" ]] && rm -rf "$DEPLOY_PATH" - mv "$BACKUP_PATH" "$DEPLOY_PATH" - cd "$DEPLOY_PATH" && docker compose restart - } -} - -# Atomic Deployment mit Git -atomic_git_deployment() { - log "Starte atomic git deployment..." - - # 1. Backup der aktuellen Version erstellen - if [[ -d "$DEPLOY_PATH" ]]; then - log "Erstelle Backup der aktuellen Version..." - cp -r "$DEPLOY_PATH" "$BACKUP_PATH" - success "Backup erstellt: $BACKUP_PATH" - fi - - # 2. Neue Version in temporäres Verzeichnis clonen - log "Clone neue Version mit Tag: $DEPLOY_TAG" - - if [[ -d "$TEMP_PATH" ]]; then - rm -rf "$TEMP_PATH" - fi - - # Git Repository klonen oder pullen - if [[ -d "$DEPLOY_PATH/.git" ]]; then - # Existierendes Repository - fetch und checkout - log "Update existierendes Repository..." - cd "$DEPLOY_PATH" - git fetch --tags origin - git checkout "$DEPLOY_TAG" - success "Git checkout zu $DEPLOY_TAG erfolgreich" - else - # Neues Repository klonen - log "Klone Repository neu..." - git clone --depth 1 --branch "$DEPLOY_TAG" "$(git -C . remote get-url origin)" "$TEMP_PATH" || { - error "Git clone fehlgeschlagen" - } - - # Atomic Switch: Temp → Live - if [[ -d "$DEPLOY_PATH" ]]; then - mv "$DEPLOY_PATH" "${DEPLOY_PATH}-old" - fi - mv "$TEMP_PATH" "$DEPLOY_PATH" - fi -} - -# Framework-spezifische Deployment-Schritte -framework_deployment() { - log "Führe Framework-Deployment durch..." - - cd "$DEPLOY_PATH" - - # 1. Environment Setup - if [[ ! -f .env ]]; then - if [[ -f .env.production ]]; then - log "Kopiere .env.production zu .env" - cp .env.production .env - else - error ".env.production nicht gefunden" - fi - fi - - # 2. Composer Dependencies (Production) - log "Installiere Composer Dependencies..." - if ! composer install --no-dev --optimize-autoloader --no-interaction; then - error "Composer Install fehlgeschlagen" - fi - - # 3. Framework Cache warming (falls implementiert) - if [[ -f console.php ]] && php console.php list | grep -q "cache:warm"; then - log "Wärme Framework Caches auf..." - php console.php cache:warm || warning "Cache warming fehlgeschlagen" - fi - - # 4. Database Migrations - log "Führe Database Migrations durch..." - if php console.php list | grep -q "db:migrate"; then - php console.php db:migrate || error "Database Migration fehlgeschlagen" - else - warning "Keine Database Migrations gefunden" - fi - - # 5. Framework optimizations - if php console.php list | grep -q "framework:optimize"; then - log "Optimiere Framework..." - php console.php framework:optimize || warning "Framework Optimization fehlgeschlagen" - fi - - success "Framework-Deployment abgeschlossen" -} - -# Docker Services Management -manage_docker_services() { - log "Manage Docker Services..." - - cd "$DEPLOY_PATH" - - # Prüfe ob docker-compose.yml existiert - if [[ ! -f docker-compose.yml ]]; then - error "docker-compose.yml nicht gefunden" - fi - - # Services neu starten mit minimaler Downtime - log "Starte Services neu..." - - # Rolling Update Strategy - docker compose pull --quiet || warning "Docker Pull Warnings (nicht kritisch)" - - # Restart mit Health Checks - if docker compose up -d --force-recreate --remove-orphans; then - success "Docker Services erfolgreich neu gestartet" - else - error "Docker Restart fehlgeschlagen" - fi - - # Warte auf Service-Start - log "Warte auf Service-Start..." - sleep 10 -} - -# Health Checks -health_checks() { - log "Führe Health Checks durch..." - - cd "$DEPLOY_PATH" - - # 1. Framework Health Check - if php console.php health:check; then - success "Framework Health Check OK" - else - error "Framework Health Check fehlgeschlagen" - fi - - # 2. Docker Services Check - if ! docker compose ps | grep -q "Up"; then - error "Docker Services nicht verfügbar" - fi - success "Docker Services OK" - - # 3. MCP Server Test (optional) - if timeout 5 php console.php mcp:server --test 2>/dev/null; then - success "MCP Server OK" - else - log "MCP Server Test übersprungen (optional)" - fi - - # 4. Web Response Test - sleep 5 - if curl -f -s -H "User-Agent: Mozilla/5.0 (Deployment Health Check)" "http://localhost" > /dev/null; then - success "HTTP Response OK" - else - warning "HTTP Response Test fehlgeschlagen (möglicherweise nginx-Problem)" - fi -} - -# Cleanup alte Backups (behalte nur die letzten 5) -cleanup_old_backups() { - log "Räume alte Backups auf..." - - # Finde alle Backup-Ordner und behalte nur die 5 neuesten - find "$(dirname "$DEPLOY_PATH")" -maxdepth 1 -name "*-backup-*" -type d | \ - sort -r | \ - tail -n +6 | \ - xargs -r rm -rf - - success "Backup-Cleanup abgeschlossen" -} - -# Deployment-Summary -deployment_summary() { - echo -e "${GREEN}" - echo "🎉 Server-Deployment erfolgreich!" - echo "📋 Tag: $DEPLOY_TAG" - echo "📁 Path: $DEPLOY_PATH" - echo "💾 Backup: $BACKUP_PATH" - echo "⏰ Zeit: $(date)" - echo -e "${RESET}" -} - -# Hauptprogramm -main() { - log "🚀 Server-side Deployment gestartet" - log "📦 Tag: $DEPLOY_TAG" - log "📁 Path: $DEPLOY_PATH" - - atomic_git_deployment - framework_deployment - manage_docker_services - health_checks - cleanup_old_backups - deployment_summary - - # Cleanup erfolgreicher Deployment - [[ -d "${DEPLOY_PATH}-old" ]] && rm -rf "${DEPLOY_PATH}-old" -} - -# Error Handling -trap cleanup_on_error ERR - -# Script ausführen -main "$@" \ No newline at end of file diff --git a/scripts/setup-gitea-secrets-interactive.sh b/scripts/setup-gitea-secrets-interactive.sh new file mode 100755 index 00000000..ad1575eb --- /dev/null +++ b/scripts/setup-gitea-secrets-interactive.sh @@ -0,0 +1,123 @@ +#!/bin/bash +# Interactive Script to set Gitea Repository Secrets via API +# Usage: ./scripts/setup-gitea-secrets-interactive.sh + +set -euo pipefail + +GITEA_URL="${GITEA_URL:-https://git.michaelschiemer.de}" +REPO_OWNER="${REPO_OWNER:-michael}" +REPO_NAME="${REPO_NAME:-michaelschiemer}" + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +echo -e "${BLUE}=== Gitea Repository Secrets Setup ===${NC}" +echo "" +echo "Repository: ${REPO_OWNER}/${REPO_NAME}" +echo "Gitea URL: ${GITEA_URL}" +echo "" + +# Check for existing token +if [ -z "${GITEA_TOKEN:-}" ]; then + echo -e "${YELLOW}Gitea Access Token benötigt${NC}" + echo "" + echo "Bitte generiere einen Token:" + echo "1. Gehe zu: ${GITEA_URL}/user/settings/applications" + echo "2. Klicke 'Generate New Token'" + echo "3. Name: 'secrets-setup'" + echo "4. Scopes: 'write:repository' (oder alle)" + echo "5. Kopiere den Token" + echo "" + read -sp "Gitea Token: " GITEA_TOKEN + echo "" + echo "" +fi + +if [ -z "${GITEA_TOKEN:-}" ]; then + echo -e "${RED}❌ Token erforderlich - Abbruch${NC}" + exit 1 +fi + +# Function to create/update secret via API +set_secret() { + local secret_name=$1 + local secret_value=$2 + + echo -n "Setting $secret_name... " + + # Gitea API endpoint: PUT /repos/{owner}/{repo}/actions/secrets/{secretname} + local response=$(curl -s -w "\n%{http_code}" \ + -X PUT \ + -H "Authorization: token ${GITEA_TOKEN}" \ + -H "Content-Type: application/json" \ + "${GITEA_URL}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/actions/secrets/${secret_name}" \ + -d "{ + \"data\": \"$(printf '%s' "$secret_value" | base64 | tr -d '\n')\" + }" 2>&1) + + local http_code=$(echo "$response" | tail -n1) + local body=$(echo "$response" | sed '$d') + + if [ "$http_code" = "204" ] || [ "$http_code" = "201" ]; then + echo -e "${GREEN}✅ OK${NC}" + return 0 + elif [ "$http_code" = "404" ]; then + echo -e "${YELLOW}⚠️ Repository oder Token-Berechtigung fehlt${NC}" + return 1 + else + echo -e "${RED}❌ FAILED (HTTP $http_code)${NC}" + echo "Response: $body" + return 1 + fi +} + +# Get registry password (default) +REGISTRY_PASSWORD="${REGISTRY_PASSWORD:-registry-secure-password-2025}" + +# Get SSH private key +if [ -f ~/.ssh/production ]; then + SSH_PRIVATE_KEY=$(cat ~/.ssh/production) + echo -e "${GREEN}✓ SSH private key gefunden${NC}" +else + echo -e "${RED}✗ SSH private key nicht gefunden in ~/.ssh/production${NC}" + exit 1 +fi + +echo "" +echo "Setting secrets for repository: ${REPO_OWNER}/${REPO_NAME}" +echo "" + +# Test API connection first +echo -n "Testing API connection... " +test_response=$(curl -s -o /dev/null -w "%{http_code}" \ + -H "Authorization: token ${GITEA_TOKEN}" \ + "${GITEA_URL}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}") + +if [ "$test_response" != "200" ]; then +者可 echo -e "${RED}❌ FAILED (HTTP $test_response)${NC}" + echo "" + echo "Mögliche Probleme:" + echo "- Token ungültig oder fehlende Berechtigungen" + echo "- Repository nicht gefunden: ${REPO_OWNER}/${REPO_NAME}" + echo "- Netzwerkproblem" + exit 1 +fi + +echo -e "${GREEN}✅ OK${NC}" +echo "" + +# Set secrets +set_secret "REGISTRY_USER" "admin" +set_secret "REGISTRY_PASSWORD" "$REGISTRY_PASSWORD" +set_secret "SSH_PRIVATE_KEY" "$SSH_PRIVATE_KEY" + +echo "" +echo -e "${GREEN}=== Secrets Setup Complete ===${NC}" +echo "" +echo "Prüfe Secrets in Gitea UI:" +echo "${GITEA_URL}/${REPO_OWNER}/${REPO_NAME}/settings/secrets/actions" +echo "" diff --git a/scripts/setup-gitea-secrets-with-token.sh b/scripts/setup-gitea-secrets-with-token.sh new file mode 100755 index 00000000..431e5ba2 --- /dev/null +++ b/scripts/setup-gitea-secrets-with-token.sh @@ -0,0 +1,120 @@ +#!/bin/bash +# Set Gitea Repository Secrets with Token +# Usage: ./scripts/setup-gitea-secrets-with-token.sh +# or: GITEA_TOKEN=xxx ./scripts/setup-gitea-secrets-with-token.sh + +set -euo pipefail + +GITEA_URL="${GITEA_URL:-https://git.michaelschiemer.de}" +REPO_OWNER="${REPO_OWNER:-michael}" +REPO_NAME="${REPO_NAME:-michaelschiemer}" +GITEA_TOKEN="${1:-${GITEA_TOKEN:-}}" + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +echo -e "${BLUE}=== Gitea Repository Secrets Setup ===${NC}" +echo "" +echo "Repository: ${REPO_OWNER}/${REPO_NAME}" +echo "Gitea URL: ${GITEA_URL}" +echo "" + +# Check if token is provided +if [ -z "$GITEA_TOKEN" ]; then + echo -e "${RED}❌ Fehler: GITEA_TOKEN nicht angegeben${NC}" + echo "" + echo "Verwendung:" + echo " $0 " + echo " oder:" + echo " GITEA_TOKEN= $0" + echo "" + exit 1 +fi + +# Function to create/update secret via API +set_secret() { + local secret_name=$1 + local secret_value=$2 + + echo -n "Setting $secret_name... " + + # Base64 encode the secret value + local encoded_value=$(printf '%s' "$secret_value" | base64 | tr -d '\n') + + # Gitea API endpoint: PUT /repos/{owner}/{repo}/actions/secrets/{secretname} + local response=$(curl -s -w "\n%{http_code}" \ + -X PUT \ + -H "Authorization: token ${GITEA_TOKEN}" \ + -H "Content-Type: application/json" \ + "${GITEA_URL}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/actions/secrets/${secret_name}" \ + -d "{ + \"data\": \"${encoded_value}\" + }" 2>&1) + + local http_code=$(echo "$response" | tail -n1) + local body=$(echo "$response" | sed '$d') + + if [ "$http_code" = "204" ] || [ "$http_code" = "201" ]; then + echo -e "${GREEN}✅ OK${NC}" + return 0 + elif [ "$http_code" = "404" ]; then + echo -e "${YELLOW}⚠️ Repository oder Token-Berechtigung fehlt${NC}" + echo "Response: $body" + return 1 + else + echo -e "${RED}❌ FAILED (HTTP $http_code)${NC}" + echo "Response: $body" + return 1 + fi +} + +# Get registry password (default) +REGISTRY_PASSWORD="${REGISTRY_PASSWORD:-registry-secure-password-2025}" + +# Get SSH private key +if [ -f ~/.ssh/production ]; then + SSH_PRIVATE_KEY=$(cat ~/.ssh/production) + echo -e "${GREEN}✓ SSH private key gefunden${NC}" +else + echo -e "${RED}✗ SSH private key nicht gefunden in ~/.ssh/production${NC}" + exit 1 +fi + +echo "" +echo "Testing API connection..." + +# Test API connection first +test_response=$(curl -s -o /dev/null -w "%{http_code}" \ + -H "Authorization: token ${GITEA_TOKEN}" \ + "${GITEA_URL}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}") + +if [ "$test_response" != "200" ]; then + echo -e "${RED}❌ API-Verbindung fehlgeschlagen (HTTP $test_response)${NC}" + echo "" + echo "Mögliche Probleme:" + echo "- Token ungültig oder fehlende Berechtigungen" + echo "- Repository nicht gefunden: ${REPO_OWNER}/${REPO_NAME}" + echo "- Netzwerkproblem" + exit 1 +fi + +echo -e "${GREEN}✅ API-Verbindung erfolgreich${NC}" +echo "" +echo "Setting secrets..." +echo "" + +# Set secrets +set_secret "REGISTRY_USER" "admin" +set_secret "REGISTRY_PASSWORD" "$REGISTRY_PASSWORD" +set_secret "SSH_PRIVATE_KEY" "$SSH_PRIVATE_KEY" + +echo "" +echo -e "${GREEN}=== Secrets Setup Complete ===${NC}" +echo "" +echo "Prüfe Secrets in Gitea UI:" +echo "${GITEA_URL}/${REPO_OWNER}/${REPO_NAME}/settings/secrets/actions" +echo "" diff --git a/scripts/setup-gitea-secrets.sh b/scripts/setup-gitea-secrets.sh new file mode 100755 index 00000000..fcbccd6f --- /dev/null +++ b/scripts/setup-gitea-secrets.sh @@ -0,0 +1,96 @@ +#!/bin/bash +# Helper Script to set Gitea Repository Secrets +# Usage: ./scripts/setup-gitea-secrets.sh [GITEA_TOKEN] + +set -euo pipefail + +GITEA_URL="${GITEA_URL:-https://git.michaelschiemer.de}" +REPO_OWNER="${REPO_OWNER:-$(git config user.name || echo 'michael')}" +REPO_NAME="${REPO_NAME:-michaelschiemer}" +GITEA_TOKEN="${1:-${GITEA_TOKEN:-}}" + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +echo -e "${GREEN}=== Gitea Repository Secrets Setup ===${NC}" +echo "" + +# Check if token is provided +if [ -z "$GITEA_TOKEN" ]; then + echo -e "${YELLOW}⚠️ GITEA_TOKEN nicht gesetzt${NC}" + echo "" + echo "Bitte generiere einen Gitea Access Token:" + echo "1. Gehe zu: ${GITEA_URL}/user/settings/applications" + echo "2. Klicke 'Generate New Token'" + echo "3. Name: 'secrets-setup'" + echo "4. Scopes: 'write:repository'" + echo "5. Kopiere den Token" + echo "" + echo "Dann führe aus:" + echo " export GITEA_TOKEN='dein-token'" + echo " ./scripts/setup-gitea-secrets.sh" + echo "" + exit 1 +fi + +# Function to create/update secret +set_secret() { + local secret_name=$1 + local secret_value=$2 + + echo -n "Setting $secret_name... " + + # Gitea API endpoint for repository secrets + local response=$(curl -s -w "\n%{http_code}" \ + -X PUT \ + -H "Authorization: token ${GITEA_TOKEN}" \ + -H "Content-Type: application/json" \ + "${GITEA_URL}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/actions/secrets/${secret_name}" \ + -d "{ + \"data\": \"${secret_value}\" + }") + + local http_code=$(echo "$response" | tail -n1) + local body=$(echo "$response" | sed '$d') + + if [ "$http_code" = "204" ] || [ "$http_code" = "201" ]; then + echo -e "${GREEN}✅ OK${NC}" + return 0 + else + echo -e "${RED}❌ FAILED (HTTP $http_code)${NC}" + echo "Response: $body" + return 1 + fi +} + +# Get registry password (default or from vault) +REGISTRY_PASSWORD="${REGISTRY_PASSWORD:-registry-secure-password-2025}" + +# Get SSH private key +if [ -f ~/.ssh/production ]; then + SSH_PRIVATE_KEY=$(cat ~/.ssh/production) + echo -e "${GREEN}✓ SSH private key gefunden${NC}" +else + echo -e "${RED}✗ SSH private key nicht gefunden in ~/.ssh/production${NC}" + echo "Bitte SSH key Pfad anpassen oder manuell setzen" + exit 1 +fi + +echo "" +echo "Setting secrets for repository: ${REPO_OWNER}/${REPO_NAME}" +echo "" + +# Set secrets +set_secret "REGISTRY_USER" "admin" +set_secret "REGISTRY_PASSWORD" "$REGISTRY_PASSWORD" +set_secret "SSH_PRIVATE_KEY" "$SSH_PRIVATE_KEY" + +echo "" +echo -e "${GREEN}=== Secrets Setup Complete ===${NC}" +echo "" +echo "Prüfe Secrets in Gitea UI:" +echo "${GITEA_URL}/${REPO_OWNER}/${REPO_NAME}/enu/repo/settings/secrets" +echo "" diff --git a/scripts/setup-production-secrets.sh b/scripts/setup-production-secrets.sh new file mode 100755 index 00000000..0d2507bc --- /dev/null +++ b/scripts/setup-production-secrets.sh @@ -0,0 +1,85 @@ +#!/bin/bash +# ============================================================================== +# Production Secrets Setup Script +# ============================================================================== +# This script creates Docker Secrets on the production server from .env values +# Run this ONCE during initial setup on the production server. +# ============================================================================== + +set -e + +echo "🔐 Docker Secrets Setup for Production" +echo "======================================" +echo "" + +# Check if running on production server +if [ ! -f /home/deploy/framework/.env ]; then + echo "❌ ERROR: /home/deploy/framework/.env not found" + echo " Please ensure .env file exists on production server" + exit 1 +fi + +# Check if Docker Swarm is initialized +if ! docker info | grep -q "Swarm: active"; then + echo "❌ ERROR: Docker Swarm is not initialized" + echo " Run: docker swarm init" + exit 1 +fi + +echo "📋 Reading secrets from .env file..." +cd /home/deploy/framework + +# Function to create secret from .env +create_secret() { + local secret_name=$1 + local env_key=$2 + + # Extract value from .env + local value=$(grep "^${env_key}=" .env | cut -d'=' -f2- | sed 's/^"\(.*\)"$/\1/') + + if [ -z "$value" ]; then + echo "⚠️ WARNING: ${env_key} not found in .env, skipping ${secret_name}" + return + fi + + # Check if secret already exists + if docker secret ls --format "{{.Name}}" | grep -q "^${secret_name}$"; then + echo "ℹ️ Secret '${secret_name}' already exists, skipping..." + return + fi + + # Create secret + echo "$value" | docker secret create "$secret_name" - 2>/dev/null + + if [ $? -eq 0 ]; then + echo "✅ Created secret: ${secret_name}" + else + echo "❌ Failed to create secret: ${secret_name}" + fi +} + +echo "" +echo "🔑 Creating Docker Secrets..." +echo "" + +# Create all required secrets +create_secret "db_password" "DB_PASSWORD" +create_secret "app_key" "APP_KEY" +create_secret "vault_encryption_key" "VAULT_ENCRYPTION_KEY" +create_secret "shopify_webhook_secret" "SHOPIFY_WEBHOOK_SECRET" +create_secret "rapidmail_password" "RAPIDMAIL_PASSWORD" + +echo "" +echo "📊 Verifying Secrets..." +echo "" + +docker secret ls + +echo "" +echo "✅ Secrets setup completed!" +echo "" +echo "Next steps:" +echo " 1. Deploy the stack: docker stack deploy -c docker-compose.prod.yml framework" +echo " 2. Monitor deployment: watch docker stack ps framework" +echo " 3. Check logs: docker service logs framework_web" +echo ""