refactor: clean repository structure for v0.1.0 open source release
- Remove Go artifacts (cmd/, internal/, pkg/, go.mod) - Move furt-lua/* content to repository root - Restructure as clean src/, config/, scripts/, tests/ layout - Rewrite README.md as practical tool documentation - Remove timeline references and marketing language - Clean .gitignore from Go-era artifacts - Update config/server.lua with example.org defaults - Add .env.production to .gitignore for security Repository now ready for open source distribution with minimal, focused structure and generic configuration templates. close issue DAW/furt#86
This commit is contained in:
parent
87c935379b
commit
be3b9614d0
38 changed files with 280 additions and 5892 deletions
37
.gitignore
vendored
37
.gitignore
vendored
|
|
@ -1,37 +1,16 @@
|
||||||
# Environment variables (NEVER commit!)
|
# Environment variables (NEVER commit!)
|
||||||
.env
|
.env
|
||||||
|
|
||||||
# Go build artifacts
|
|
||||||
*.exe
|
|
||||||
*.exe~
|
|
||||||
*.dll
|
|
||||||
*.so
|
|
||||||
*.dylib
|
|
||||||
furt-gateway
|
|
||||||
formular2mail-service
|
|
||||||
sagjan-service
|
|
||||||
/build/
|
|
||||||
/dist/
|
|
||||||
|
|
||||||
# Go test files
|
|
||||||
*.test
|
|
||||||
*.out
|
|
||||||
coverage.txt
|
|
||||||
coverage.html
|
|
||||||
|
|
||||||
# Go modules
|
|
||||||
/vendor/
|
|
||||||
|
|
||||||
# Lua specific
|
# Lua specific
|
||||||
*.luac
|
*.luac
|
||||||
.luarocks/
|
.luarocks/
|
||||||
luarocks.lock
|
luarocks.lock
|
||||||
|
|
||||||
# Furt-lua runtime/build artifacts
|
# Furt runtime/build artifacts
|
||||||
furt-lua/bin/
|
bin/
|
||||||
furt-lua/logs/
|
logs/
|
||||||
furt-lua/tmp/
|
tmp/
|
||||||
furt-lua/pid/
|
pid/
|
||||||
|
|
||||||
# Issue creation scripts (these create issues, don't version them)
|
# Issue creation scripts (these create issues, don't version them)
|
||||||
scripts/gitea-issues/
|
scripts/gitea-issues/
|
||||||
|
|
@ -74,8 +53,6 @@ debug.log
|
||||||
*.sqlite3
|
*.sqlite3
|
||||||
|
|
||||||
# Configuration files with secrets
|
# Configuration files with secrets
|
||||||
config.local.yaml
|
config.local.lua
|
||||||
config.production.yaml
|
config.production.lua
|
||||||
furt-lua/config/local.lua
|
|
||||||
furt-lua/config/production.lua
|
|
||||||
|
|
||||||
|
|
|
||||||
235
README.md
235
README.md
|
|
@ -1,145 +1,160 @@
|
||||||
# Furt API Gateway
|
# Furt API Gateway
|
||||||
|
|
||||||
**Low-Tech API-Gateway für digitale Souveränität**
|
**HTTP-Server in Lua für Service-Integration**
|
||||||
*Von Go zu C+Lua - Corporate-freie Technologie-Migration*
|
|
||||||
|
|
||||||
## Überblick
|
## Überblick
|
||||||
|
|
||||||
Furt ist ein minimalistischer API-Gateway, der verschiedene Services unter einer einheitlichen API vereint. Der Name "Furt" (germanisch für "Durchgang durch Wasser") symbolisiert die Gateway-Funktion: Alle Requests durchqueren die API-Furt um zu den dahinterliegenden Services zu gelangen.
|
Furt ist ein HTTP-Server der verschiedene Services unter einer API vereint. Aktuell unterstützt es Mail-Versendung über SMTP und bietet eine einfache JSON-API für Web-Integration.
|
||||||
|
|
||||||
## Technologie-Migration
|
## Features
|
||||||
|
|
||||||
🔄 **Strategische Neuausrichtung (Juni 2025):**
|
- HTTP-Server mit JSON-APIs
|
||||||
- **Von:** Go-basierte Implementation (Corporate-controlled)
|
- Mail-Versendung über SMTP
|
||||||
- **Zu:** C + Lua Implementation (maximale Souveränität)
|
- Request-Routing und Authentication
|
||||||
- **Grund:** Elimination von Google-Dependencies für echte digitale Unabhängigkeit
|
- Health-Check-Endpoints
|
||||||
|
- Konfigurierbare Rate-Limiting
|
||||||
|
- Hugo/Website-Integration
|
||||||
|
|
||||||
## Aktuelle Implementierungen
|
## Dependencies
|
||||||
|
|
||||||
### 🆕 furt-lua (Aktiv entwickelt)
|
**Erforderlich:**
|
||||||
**Pure Lua HTTP-Server - Week 1 ✅**
|
- `lua` 5.4+
|
||||||
- ✅ HTTP-Server mit lua-socket
|
- `lua-socket` (HTTP-Server)
|
||||||
- ✅ JSON API-Endpoints
|
- `lua-cjson` (JSON-Verarbeitung)
|
||||||
- ✅ Basic Routing und Error-Handling
|
|
||||||
- ✅ Mail-Service-Grundgerüst
|
|
||||||
- 🔄 SMTP-Integration (Week 2)
|
|
||||||
|
|
||||||
|
**Installation:**
|
||||||
```bash
|
```bash
|
||||||
cd furt-lua/
|
# Arch Linux
|
||||||
./scripts/start.sh
|
|
||||||
# Server: http://127.0.0.1:8080
|
|
||||||
```
|
|
||||||
|
|
||||||
### 📦 Go-Implementation (Parallel/Legacy)
|
|
||||||
- Ursprüngliche Planung in `cmd/`, `internal/`
|
|
||||||
- Wird durch Lua-Version ersetzt
|
|
||||||
- Referenz für API-Kompatibilität
|
|
||||||
|
|
||||||
## Philosophie
|
|
||||||
|
|
||||||
- **Technologie-Souveränität**: Nur akademische/unabhängige Technologien
|
|
||||||
- **Low-Tech-Ansatz**: C + Lua statt Corporate-Runtimes
|
|
||||||
- **Minimale Dependencies**: < 5 externe Libraries
|
|
||||||
- **Modulare Architektur**: < 200 Zeilen pro Modul
|
|
||||||
- **Vollständige Transparenz**: Jede Zeile Code verstehbar
|
|
||||||
- **Langfristige Stabilität**: 50+ Jahre bewährte Technologien
|
|
||||||
|
|
||||||
## Tech-Stack (Final)
|
|
||||||
|
|
||||||
**Souveräne Technologien:**
|
|
||||||
- **C** (GCC + musl) - Kern-Performance
|
|
||||||
- **Lua** (PUC-Rio University) - Business-Logic
|
|
||||||
- **LMDB** (Howard Chu/Symas) - Datenbank
|
|
||||||
- **OpenBSD httpd** - Reverse-Proxy (langfristig)
|
|
||||||
|
|
||||||
**Corporate-frei:** Keine Google-, Microsoft-, oder VC-kontrollierten Dependencies
|
|
||||||
|
|
||||||
## Services
|
|
||||||
|
|
||||||
- **formular2mail**: Kontaktformulare zu E-Mail (Week 1 ✅)
|
|
||||||
- **sagjan**: Selbst-gehostetes Kommentarsystem
|
|
||||||
- **lengan**: Projektverwaltung
|
|
||||||
- **budlam**: Kontaktverwaltung
|
|
||||||
- **Weitere**: Shop, Newsletter, Kalendar, etc.
|
|
||||||
|
|
||||||
## Installation & Entwicklung
|
|
||||||
|
|
||||||
### Quick Start (furt-lua)
|
|
||||||
```bash
|
|
||||||
# Dependencies (Arch Linux)
|
|
||||||
pacman -S lua lua-socket lua-cjson
|
pacman -S lua lua-socket lua-cjson
|
||||||
|
|
||||||
# Start Development-Server
|
# Ubuntu/Debian
|
||||||
cd furt-lua/
|
apt install lua5.4 lua-socket lua-cjson
|
||||||
chmod +x scripts/start.sh
|
|
||||||
./scripts/start.sh
|
|
||||||
|
|
||||||
# Test
|
|
||||||
curl -X POST http://127.0.0.1:8080/test \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"test":"data"}'
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Testing
|
## Installation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Automated Tests
|
# Repository klonen
|
||||||
cd furt-lua/
|
git clone <repository-url>
|
||||||
lua tests/test_http.lua
|
cd furt
|
||||||
|
|
||||||
# Manual curl Tests
|
# Scripts ausführbar machen
|
||||||
./scripts/test_curl.sh
|
chmod +x scripts/*.sh
|
||||||
|
|
||||||
|
# Server starten
|
||||||
|
./scripts/start.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
## Roadmap
|
**Server läuft auf:** http://127.0.0.1:8080
|
||||||
|
|
||||||
### Phase 1: Lua-Foundation (4 Wochen) ✅
|
## API-Endpoints
|
||||||
- [x] Week 1: HTTP-Server + Mail-Service-Grundgerüst
|
|
||||||
- [ ] Week 2: SMTP-Integration + API-Key-Auth
|
|
||||||
- [ ] Week 3: Service-Expansion (Comments)
|
|
||||||
- [ ] Week 4: Production-Ready (HTTPS, Systemd)
|
|
||||||
|
|
||||||
### Phase 2: C-Integration (4-6 Wochen)
|
### Health Check
|
||||||
- [ ] C-HTTP-Server für Performance
|
```bash
|
||||||
- [ ] C ↔ Lua Bridge
|
GET /health
|
||||||
- [ ] Memory-Management + Security-Hardening
|
→ {"status":"healthy","service":"furt","version":"1.0.0"}
|
||||||
|
```
|
||||||
|
|
||||||
### Phase 3: Infrastructure-Migration (6-12 Monate)
|
### Mail senden
|
||||||
- [ ] OpenBSD-Migration
|
```bash
|
||||||
- [ ] ISPConfig → eigene Scripts
|
POST /v1/mail/send
|
||||||
- [ ] Apache → OpenBSD httpd
|
Content-Type: application/json
|
||||||
|
|
||||||
## Dokumentation
|
{
|
||||||
|
"name": "Name",
|
||||||
|
"email": "sender@example.com",
|
||||||
|
"message": "Nachricht"
|
||||||
|
}
|
||||||
|
|
||||||
**Development:**
|
→ {"success":true,"message":"Mail sent"}
|
||||||
- [`devdocs/furt_konzept.md`](devdocs/furt_konzept.md) - Technische Architektur
|
```
|
||||||
- [`devdocs/furt_master_strategy.md`](devdocs/furt_master_strategy.md) - 18-24 Monate Roadmap
|
|
||||||
- [`devdocs/furt_development_process.md`](devdocs/furt_development_process.md) - Development-Guidelines
|
|
||||||
|
|
||||||
**API:**
|
## Konfiguration
|
||||||
- [`furt-lua/README.md`](furt-lua/README.md) - Lua-Implementation Details
|
|
||||||
- `docs/api/` - API-Dokumentation (in Entwicklung)
|
|
||||||
|
|
||||||
## Technologie-Rationale
|
**Environment Variables (.env):**
|
||||||
|
```bash
|
||||||
|
FURT_MAIL_HOST=mail.example.com
|
||||||
|
FURT_MAIL_PORT=587
|
||||||
|
FURT_MAIL_USERNAME=user@example.com
|
||||||
|
FURT_MAIL_PASSWORD=password
|
||||||
|
FURT_MAIL_TO=empfaenger@example.com
|
||||||
|
```
|
||||||
|
|
||||||
**Warum Lua statt Go?**
|
**Server-Config (config/server.lua):**
|
||||||
- Go = Google-controlled (Module-Proxy, Telemetrie)
|
- Port und Host-Einstellungen
|
||||||
- Lua = PUC-Rio University (echte Unabhängigkeit)
|
- API-Key-Konfiguration
|
||||||
- C + Lua = 50+ Jahre bewährt vs. Corporate-Runtime
|
- Rate-Limiting-Parameter
|
||||||
- Performance: 10x weniger Memory, 5x weniger CPU
|
|
||||||
|
|
||||||
**Teil der Dragons@Work Digital-Sovereignty-Strategie**
|
## Testing
|
||||||
|
|
||||||
## Status
|
**Automatische Tests:**
|
||||||
|
```bash
|
||||||
|
lua tests/test_http.lua
|
||||||
|
```
|
||||||
|
|
||||||
🚀 **Week 1 Complete:** Lua HTTP-Server funktional
|
**Manuelle Tests:**
|
||||||
🔄 **Week 2 Active:** SMTP-Integration + Hugo-Integration
|
```bash
|
||||||
📋 **Week 3+ Planned:** Service-Expansion + C-Migration
|
./scripts/test_curl.sh
|
||||||
|
|
||||||
## Lizenz
|
# Oder direkt:
|
||||||
|
curl -X POST http://127.0.0.1:8080/v1/mail/send \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"name":"Test","email":"test@example.com","message":"Test"}'
|
||||||
|
```
|
||||||
|
|
||||||
Apache License 2.0 - Siehe [LICENSE](LICENSE) für Details.
|
## Deployment
|
||||||
|
|
||||||
---
|
**OpenBSD:**
|
||||||
|
- rc.d-Script in `deployment/openbsd/`
|
||||||
|
- Systemd-Integration über Scripts
|
||||||
|
|
||||||
*Furt steht im Einklang mit den Prinzipien digitaler Souveränität und dem Low-Tech-Ansatz des Dragons@Work-Projekts.*
|
**Production-Setup:**
|
||||||
|
```bash
|
||||||
|
# Environment-Config kopieren
|
||||||
|
cp .env.example .env.production
|
||||||
|
# → SMTP-Credentials anpassen
|
||||||
|
|
||||||
|
# Production-Mode starten
|
||||||
|
export FURT_ENV=production
|
||||||
|
./scripts/start.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Projektstruktur
|
||||||
|
|
||||||
|
```
|
||||||
|
furt/
|
||||||
|
├── src/ # Lua-Source-Code
|
||||||
|
│ ├── main.lua # HTTP-Server
|
||||||
|
│ ├── routes/ # API-Endpoints
|
||||||
|
│ └── smtp.lua # Mail-Integration
|
||||||
|
├── config/ # Konfiguration
|
||||||
|
├── scripts/ # Start/Test-Scripts
|
||||||
|
├── tests/ # Test-Suite
|
||||||
|
└── deployment/ # System-Integration
|
||||||
|
```
|
||||||
|
|
||||||
|
## Hugo-Integration
|
||||||
|
|
||||||
|
**Shortcode-Beispiel:**
|
||||||
|
```html
|
||||||
|
<form action="http://your-server:8080/v1/mail/send" method="POST">
|
||||||
|
<input name="name" type="text" required>
|
||||||
|
<input name="email" type="email" required>
|
||||||
|
<textarea name="message" required></textarea>
|
||||||
|
<button type="submit">Senden</button>
|
||||||
|
</form>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
**Code-Struktur:**
|
||||||
|
- Module unter 200 Zeilen
|
||||||
|
- Funktionen unter 50 Zeilen
|
||||||
|
- Klare Fehlerbehandlung
|
||||||
|
- Testbare Komponenten
|
||||||
|
|
||||||
|
**Dependencies minimal halten:**
|
||||||
|
- Nur lua-socket und lua-cjson
|
||||||
|
- Keine externen HTTP-Libraries
|
||||||
|
- Standard-Lua-Funktionen bevorzugen
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
-- furt-lua/config/server.lua
|
-- config/server.lua
|
||||||
-- Server configuration for Furt Lua HTTP-Server
|
-- Server configuration for Furt Lua HTTP-Server
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -76,13 +76,13 @@ return {
|
||||||
|
|
||||||
-- Mail configuration (for SMTP integration)
|
-- Mail configuration (for SMTP integration)
|
||||||
mail = {
|
mail = {
|
||||||
smtp_server = os.getenv("SMTP_HOST") or "mail.dragons-at-work.de",
|
smtp_server = os.getenv("SMTP_HOST") or "mail.example.org",
|
||||||
smtp_port = tonumber(os.getenv("SMTP_PORT")) or 465,
|
smtp_port = tonumber(os.getenv("SMTP_PORT")) or 465,
|
||||||
use_ssl = true,
|
use_ssl = true,
|
||||||
username = os.getenv("SMTP_USERNAME"),
|
username = os.getenv("SMTP_USERNAME"),
|
||||||
password = os.getenv("SMTP_PASSWORD"),
|
password = os.getenv("SMTP_PASSWORD"),
|
||||||
from_address = os.getenv("SMTP_FROM") or "noreply@dragons-at-work.de",
|
from_address = os.getenv("SMTP_FROM") or "noreply@example.org",
|
||||||
to_address = os.getenv("SMTP_TO") or "michael@dragons-at-work.de"
|
to_address = os.getenv("SMTP_TO") or "admin@example.org"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1,58 +0,0 @@
|
||||||
# Central Label Registry for Furt API Gateway Project
|
|
||||||
# Format: name:color:context:usage_contexts
|
|
||||||
# This file is the single source of truth for all labels
|
|
||||||
|
|
||||||
# === CORE WORKFLOW LABELS ===
|
|
||||||
service-request:7057ff:new_service:service_templates,status_updates
|
|
||||||
enhancement:84b6eb:improvement:all_templates
|
|
||||||
bug:d73a4a:error:bug_template,status_updates
|
|
||||||
question:d876e3:discussion:question_template
|
|
||||||
|
|
||||||
# === COMPONENT CATEGORIES ===
|
|
||||||
gateway:0052cc:gateway_core:architecture_template,performance_template,service_templates
|
|
||||||
performance:fbca04:optimization:performance_template,architecture_template
|
|
||||||
architecture:d4c5f9:design:architecture_template,gateway
|
|
||||||
security:28a745:security_review:security_template,architecture_template
|
|
||||||
configuration:f9d71c:config_management:deployment_template,architecture_template
|
|
||||||
|
|
||||||
# === SERVICE-SPECIFIC LABELS ===
|
|
||||||
service-debug-check-final2:1d76db:service_integration:service_specific
|
|
||||||
service-clean-test4:1d76db:service_integration:service_specific
|
|
||||||
service-debug-test:1d76db:service_integration:service_specific
|
|
||||||
service-formular2mail:1d76db:formular2mail:formular2mail_integration
|
|
||||||
service-sagjan:1d76db:sagjan:sagjan_integration
|
|
||||||
service-newsletter:ff6b6b:newsletter:newsletter_integration
|
|
||||||
service-analytics:1d76db:service_integration:service_specific
|
|
||||||
service-whatever-you-want:1d76db:service_integration:service_specific
|
|
||||||
service-completely-absolut-new7:1d76db:service_integration:service_specific
|
|
||||||
service-completely-absolut-new8:1d76db:service_integration:service_specific
|
|
||||||
service-completely-absolut-new9:1d76db:service_integration:service_specific
|
|
||||||
service-completely-absolut-new10:1d76db:service_integration:service_specific
|
|
||||||
service-completely-absolut-new11:1d76db:service_integration:service_specific
|
|
||||||
|
|
||||||
# === WORKFLOW STATE LABELS ===
|
|
||||||
work-in-progress:fbca04:active:status_updates
|
|
||||||
needs-review:0e8a16:review:status_updates
|
|
||||||
blocked:d73a4a:blocked:status_updates
|
|
||||||
ready-for-deployment:28a745:deploy_ready:status_updates
|
|
||||||
|
|
||||||
# === INTEGRATION LABELS ===
|
|
||||||
hugo-integration:ff7518:frontend:hugo_templates,integration
|
|
||||||
api-contract:5319e7:api_design:api_templates,service_templates
|
|
||||||
breaking-change:d73a4a:breaking:api_templates,architecture_template
|
|
||||||
|
|
||||||
# === PRIORITY LABELS ===
|
|
||||||
high-priority:d73a4a:urgent:all_templates
|
|
||||||
low-priority:0e8a16:nice_to_have:all_templates
|
|
||||||
|
|
||||||
# === META LABELS ===
|
|
||||||
low-tech:6f42c1:low_tech_principle:architecture_template,performance_template,security_template
|
|
||||||
digital-sovereignty:6f42c1:digital_sovereignty:architecture_template,performance_template,security_template
|
|
||||||
good-first-issue:7057ff:beginner_friendly:manual_assignment
|
|
||||||
help-wanted:159818:community_help:manual_assignment
|
|
||||||
|
|
||||||
# === DEPLOYMENT LABELS ===
|
|
||||||
deployment:ff7518:deployment:deployment_template
|
|
||||||
testing:f9d71c:testing:testing_template,integration
|
|
||||||
|
|
||||||
test-all-templates:ff0000:test:all_templates
|
|
||||||
|
|
@ -1,590 +0,0 @@
|
||||||
# Entwicklungsprozess für Furt API-Gateway
|
|
||||||
|
|
||||||
**Erstellt:** 03.06.2025
|
|
||||||
**Letzte Aktualisierung:** 03.06.2025
|
|
||||||
**Version:** 1.0
|
|
||||||
**Verantwortlich:** Claude / DAW-Team
|
|
||||||
**Dateipfad:** devdocs/development-process.md
|
|
||||||
|
|
||||||
## Zweck dieses Dokuments
|
|
||||||
|
|
||||||
Dieses Dokument definiert den verbindlichen Prozess für die Entwicklung und Änderung von Code im Rahmen des Furt API-Gateway-Projekts. Es ergänzt die allgemeinen Entwicklungsrichtlinien um API-Gateway-spezifische Patterns und Multi-Service-Koordination.
|
|
||||||
|
|
||||||
Es richtet sich an alle Projektbeteiligten, die am Gateway oder Services entwickeln.
|
|
||||||
|
|
||||||
## Verwandte Dokumente
|
|
||||||
|
|
||||||
Dieses Dokument steht im Zusammenhang mit folgenden anderen Dokumenten:
|
|
||||||
|
|
||||||
- **KONZEPT.md:** Zentrale Referenz und Konzeptdokumentation, devdocs/KONZEPT.md
|
|
||||||
- **TESTING_GUIDELINES.md:** API-Gateway-spezifische Test-Standards, devdocs/TESTING_GUIDELINES.md
|
|
||||||
- **ARCHITECTURE.md:** Detaillierte Systemarchitektur, devdocs/ARCHITECTURE.md
|
|
||||||
|
|
||||||
## Änderungshistorie
|
|
||||||
|
|
||||||
| Version | Datum | Änderungen | Autor |
|
|
||||||
|---------|-------|------------|-------|
|
|
||||||
| 1.0 | 03.06.2025 | Initiale Version für Furt API-Gateway | Claude / DAW-Team |
|
|
||||||
|
|
||||||
## 1. Grundprinzipien für API-Gateway-Entwicklung
|
|
||||||
|
|
||||||
### 1.1 Service-First-Entwicklung
|
|
||||||
|
|
||||||
Jede Entwicklungsaufgabe muss im Kontext des **Service-Ökosystems** betrachtet werden:
|
|
||||||
|
|
||||||
- **Gateway-Änderungen** betreffen potenziell alle Services
|
|
||||||
- **Service-Änderungen** können Gateway-Anpassungen erfordern
|
|
||||||
- **API-Contracts** zwischen Gateway und Services sind kritisch
|
|
||||||
- **Breaking Changes** erfordern koordinierte Rollouts
|
|
||||||
|
|
||||||
### 1.2 API-Contract-Driven Development
|
|
||||||
|
|
||||||
Bevor Code geschrieben wird, müssen **API-Contracts** definiert werden:
|
|
||||||
|
|
||||||
- **OpenAPI-Spezifikation** für neue Endpunkte
|
|
||||||
- **Service-Interface-Definition** für neue Services
|
|
||||||
- **Authentication/Authorization-Requirements** für alle APIs
|
|
||||||
- **Error-Response-Standards** konsistent halten
|
|
||||||
|
|
||||||
### 1.3 Security-First-Pattern
|
|
||||||
|
|
||||||
Sicherheit wird bei **jeder** Änderung mitgedacht:
|
|
||||||
|
|
||||||
- **API-Key-Berechtigungen** bei neuen Endpunkten definieren
|
|
||||||
- **Input-Validation** für alle eingehenden Requests
|
|
||||||
- **Rate-Limiting** für neue Services konfigurieren
|
|
||||||
- **IP-Restrictions** wo angemessen anwenden
|
|
||||||
|
|
||||||
## 2. Verbindlicher Entwicklungsprozess für Furt
|
|
||||||
|
|
||||||
### 2.1 Vorbereitung
|
|
||||||
|
|
||||||
1. **Requirements-Analyse mit Service-Impact**
|
|
||||||
- Welche Services sind betroffen?
|
|
||||||
- Welche Gateway-Komponenten benötigen Änderungen?
|
|
||||||
- Sind Breaking Changes erforderlich?
|
|
||||||
- Welche API-Contracts müssen definiert/aktualisiert werden?
|
|
||||||
|
|
||||||
2. **Explizite Anfrage nach relevanten Dateien**
|
|
||||||
- Gateway-Dateien: `internal/gateway/`, `configs/gateway.yaml`
|
|
||||||
- Service-Dateien: `internal/services/[service]/`, `configs/services/`
|
|
||||||
- API-Dokumentation: `docs/api/`, OpenAPI-Specs
|
|
||||||
- Integration-Tests: `tests/integration/`
|
|
||||||
|
|
||||||
3. **Analyse der Service-Integration-Pattern**
|
|
||||||
- Bestehende Service-Registry-Einträge
|
|
||||||
- Routing-Patterns und Middleware-Chain
|
|
||||||
- Authentifizierungs-Flows
|
|
||||||
- Health-Check-Mechanismen
|
|
||||||
|
|
||||||
### 2.2 Design und Planung
|
|
||||||
|
|
||||||
1. **API-First-Design dokumentieren**
|
|
||||||
- OpenAPI-Spezifikation **vor** der Implementierung schreiben
|
|
||||||
- Request/Response-Schemas definieren
|
|
||||||
- HTTP-Status-Codes und Error-Handling spezifizieren
|
|
||||||
- Authentication-Requirements dokumentieren
|
|
||||||
|
|
||||||
2. **Service-Integration-Strategy festlegen**
|
|
||||||
- Wie wird der Service im Gateway registriert?
|
|
||||||
- Welche Health-Check-URL wird verwendet?
|
|
||||||
- Welche Timeout-Werte sind angemessen?
|
|
||||||
- Braucht der Service Admin-UI-Integration?
|
|
||||||
|
|
||||||
3. **Breaking-Change-Impact analysieren**
|
|
||||||
- Betrifft die Änderung bestehende API-Contracts?
|
|
||||||
- Sind koordinierte Service-Updates erforderlich?
|
|
||||||
- Müssen Client-Integrationen (Hugo-Shortcodes) angepasst werden?
|
|
||||||
- Ist eine API-Versionierung (v1 → v2) notwendig?
|
|
||||||
|
|
||||||
4. **Configuration-Strategy bestimmen**
|
|
||||||
- Welche neuen Config-Parameter werden benötigt?
|
|
||||||
- Sind Environment-Variable für Secrets erforderlich?
|
|
||||||
- Wie wird die Config zwischen Gateway und Service koordiniert?
|
|
||||||
|
|
||||||
### 2.3 Implementierung
|
|
||||||
|
|
||||||
1. **Multi-Component-Development-Order**
|
|
||||||
|
|
||||||
**Für neue Services:**
|
|
||||||
```
|
|
||||||
1. Service-Struktur scaffolden (service-generator.sh)
|
|
||||||
2. Service-Logik implementieren (Standalone-Mode)
|
|
||||||
3. Gateway-Integration hinzufügen
|
|
||||||
4. Integration-Tests schreiben
|
|
||||||
5. Deployment-Scripts anpassen
|
|
||||||
```
|
|
||||||
|
|
||||||
**Für Gateway-Änderungen:**
|
|
||||||
```
|
|
||||||
1. Gateway-Kern-Logik implementieren
|
|
||||||
2. Middleware/Auth-Anpassungen
|
|
||||||
3. Service-Integration testen
|
|
||||||
4. Health-Check-Aggregation
|
|
||||||
5. Admin-Interface-Updates
|
|
||||||
```
|
|
||||||
|
|
||||||
**Für API-Änderungen:**
|
|
||||||
```
|
|
||||||
1. OpenAPI-Spec aktualisieren
|
|
||||||
2. Gateway-Routing anpassen
|
|
||||||
3. Service-Endpunkt implementieren
|
|
||||||
4. Input-Validation hinzufügen
|
|
||||||
5. Integration-Tests erweitern
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Koordinierte Entwicklung bei Service-Updates**
|
|
||||||
- **Gateway-kompatible Änderungen zuerst** (additive APIs)
|
|
||||||
- **Service-Tests** mit Gateway-Integration
|
|
||||||
- **Backward-Compatibility** während Übergangsphase
|
|
||||||
- **Coordinated Deployment** bei Breaking Changes
|
|
||||||
|
|
||||||
3. **Configuration-Management während Entwicklung**
|
|
||||||
- **Development-Configs** in `configs/[component].dev.yaml`
|
|
||||||
- **Environment-Variable-Mapping** dokumentieren
|
|
||||||
- **Config-Validation** bei Service-Start implementieren
|
|
||||||
- **Hot-Reload** für Development (wo möglich)
|
|
||||||
|
|
||||||
### 2.4 Testing-Integration
|
|
||||||
|
|
||||||
1. **Multi-Layer-Testing-Strategy**
|
|
||||||
- **Unit-Tests:** Für Gateway- und Service-Komponenten isoliert
|
|
||||||
- **Integration-Tests:** Gateway ↔ Service-Kommunikation
|
|
||||||
- **API-Tests:** End-to-End API-Contract-Validation
|
|
||||||
- **Load-Tests:** Gateway-Performance mit mehreren Services
|
|
||||||
|
|
||||||
2. **Test-Coordination-Pattern**
|
|
||||||
```go
|
|
||||||
// Beispiel: Service-Integration-Test
|
|
||||||
func TestGatewayServiceIntegration(t *testing.T) {
|
|
||||||
// 1. Start Test-Service
|
|
||||||
service := startTestService(t, serviceConfig)
|
|
||||||
defer service.Close()
|
|
||||||
|
|
||||||
// 2. Configure Gateway with Test-Service
|
|
||||||
gateway := startTestGateway(t, gatewayConfigWithService(service.URL))
|
|
||||||
defer gateway.Close()
|
|
||||||
|
|
||||||
// 3. Test Gateway → Service communication
|
|
||||||
testServiceAPIThroughGateway(t, gateway, service)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Breaking-Change-Test-Strategy**
|
|
||||||
- **Backward-Compatibility-Tests** bei API-Änderungen
|
|
||||||
- **Version-Migration-Tests** bei Breaking Changes
|
|
||||||
- **Client-Integration-Tests** (Hugo-Shortcode-Kompatibilität)
|
|
||||||
|
|
||||||
## 3. Service-spezifische Entwicklungs-Pattern
|
|
||||||
|
|
||||||
### 3.1 Neue Service-Entwicklung
|
|
||||||
|
|
||||||
1. **Service-Scaffolding**
|
|
||||||
```bash
|
|
||||||
./scripts/service-generator.sh newsletter
|
|
||||||
# Erstellt komplette Service-Struktur
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Service-Interface-Implementation**
|
|
||||||
```go
|
|
||||||
// Jeder Service muss dieses Interface implementieren
|
|
||||||
type Service interface {
|
|
||||||
// Gateway-Integration
|
|
||||||
HandleRequest(w http.ResponseWriter, r *http.Request)
|
|
||||||
HealthCheck() HealthStatus
|
|
||||||
|
|
||||||
// Standalone-Mode
|
|
||||||
HandleWithAuth(w http.ResponseWriter, r *http.Request)
|
|
||||||
|
|
||||||
// Lifecycle
|
|
||||||
Start(ctx context.Context, config ServiceConfig) error
|
|
||||||
Stop(ctx context.Context) error
|
|
||||||
|
|
||||||
// Service-Metadata
|
|
||||||
GetServiceInfo() ServiceInfo
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Service-Registration im Gateway**
|
|
||||||
```yaml
|
|
||||||
# configs/gateway.yaml
|
|
||||||
services:
|
|
||||||
newsletter:
|
|
||||||
enabled: true
|
|
||||||
path_prefix: "/v1/newsletter"
|
|
||||||
upstream: "http://127.0.0.1:8083"
|
|
||||||
health_check: "/health"
|
|
||||||
timeout: 15s
|
|
||||||
auth_required: true
|
|
||||||
rate_limit:
|
|
||||||
requests_per_minute: 60
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3.2 Service-API-Design-Standards
|
|
||||||
|
|
||||||
1. **Einheitliche Request-Patterns**
|
|
||||||
```go
|
|
||||||
// Standard Request-Wrapper
|
|
||||||
type APIRequest struct {
|
|
||||||
RequestID string `json:"request_id,omitempty"`
|
|
||||||
Data interface{} `json:"data"`
|
|
||||||
Meta map[string]interface{} `json:"meta,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Standard Response-Wrapper
|
|
||||||
type APIResponse struct {
|
|
||||||
Success bool `json:"success"`
|
|
||||||
Data interface{} `json:"data,omitempty"`
|
|
||||||
Error *APIError `json:"error,omitempty"`
|
|
||||||
Meta map[string]interface{} `json:"meta,omitempty"`
|
|
||||||
RequestID string `json:"request_id,omitempty"`
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Konsistente Error-Handling**
|
|
||||||
```go
|
|
||||||
// Standard Error-Format
|
|
||||||
type APIError struct {
|
|
||||||
Code string `json:"code"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
Details string `json:"details,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Standard Error-Codes
|
|
||||||
const (
|
|
||||||
ErrInvalidInput = "INVALID_INPUT"
|
|
||||||
ErrUnauthorized = "UNAUTHORIZED"
|
|
||||||
ErrServiceUnavailable = "SERVICE_UNAVAILABLE"
|
|
||||||
ErrRateLimitExceeded = "RATE_LIMIT_EXCEEDED"
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Health-Check-Standards**
|
|
||||||
```go
|
|
||||||
// Standard Health-Response
|
|
||||||
type HealthStatus struct {
|
|
||||||
Status string `json:"status"`
|
|
||||||
Version string `json:"version"`
|
|
||||||
Uptime time.Duration `json:"uptime"`
|
|
||||||
Checks map[string]string `json:"checks"`
|
|
||||||
Timestamp time.Time `json:"timestamp"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Status-Werte
|
|
||||||
const (
|
|
||||||
HealthStatusHealthy = "healthy"
|
|
||||||
HealthStatusDegraded = "degraded"
|
|
||||||
HealthStatusUnhealthy = "unhealthy"
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3.3 Gateway-Integration-Pattern
|
|
||||||
|
|
||||||
1. **Service-Discovery-Integration**
|
|
||||||
```go
|
|
||||||
// Gateway registriert Services automatisch
|
|
||||||
func (g *Gateway) RegisterService(name string, config ServiceConfig) error {
|
|
||||||
service := &ServiceProxy{
|
|
||||||
Name: name,
|
|
||||||
PathPrefix: config.PathPrefix,
|
|
||||||
Upstream: config.Upstream,
|
|
||||||
HealthCheck: config.HealthCheck,
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
|
|
||||||
g.services[name] = service
|
|
||||||
g.updateRouting()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Request-Middleware-Chain**
|
|
||||||
```go
|
|
||||||
// Standard Middleware-Order für alle Services
|
|
||||||
func (g *Gateway) buildMiddlewareChain(serviceName string) []Middleware {
|
|
||||||
return []Middleware{
|
|
||||||
LoggingMiddleware,
|
|
||||||
AuthenticationMiddleware,
|
|
||||||
RateLimitingMiddleware(serviceName),
|
|
||||||
ValidationMiddleware,
|
|
||||||
ProxyMiddleware(serviceName),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 4. Configuration-Management-Pattern
|
|
||||||
|
|
||||||
### 4.1 Hierarchische Konfiguration
|
|
||||||
|
|
||||||
```go
|
|
||||||
// Config-Loading-Reihenfolge
|
|
||||||
func LoadConfig(serviceName string) (*Config, error) {
|
|
||||||
config := &Config{}
|
|
||||||
|
|
||||||
// 1. Default-Values
|
|
||||||
config.ApplyDefaults()
|
|
||||||
|
|
||||||
// 2. Base-Config-File
|
|
||||||
config.LoadFromFile("configs/" + serviceName + ".yaml")
|
|
||||||
|
|
||||||
// 3. Environment-specific
|
|
||||||
if env := os.Getenv("ENVIRONMENT"); env != "" {
|
|
||||||
config.LoadFromFile("configs/" + serviceName + "." + env + ".yaml")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Environment-Variables
|
|
||||||
config.LoadFromEnv()
|
|
||||||
|
|
||||||
// 5. Command-Line-Flags
|
|
||||||
config.LoadFromFlags()
|
|
||||||
|
|
||||||
return config.Validate()
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4.2 Service-Gateway-Config-Coordination
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# Gateway-Config für Service
|
|
||||||
services:
|
|
||||||
formular2mail:
|
|
||||||
config_sync: true
|
|
||||||
config_endpoint: "/config"
|
|
||||||
config_push_on_change: true
|
|
||||||
|
|
||||||
# Service erhält Config vom Gateway
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4.3 Secrets-Management
|
|
||||||
|
|
||||||
```go
|
|
||||||
// Secrets werden nie in Config-Files gespeichert
|
|
||||||
type ServiceConfig struct {
|
|
||||||
// Public config
|
|
||||||
Port string `yaml:"port"`
|
|
||||||
LogLevel string `yaml:"log_level"`
|
|
||||||
|
|
||||||
// Secrets via Environment
|
|
||||||
APIKey string `env:"SERVICE_API_KEY"`
|
|
||||||
DBPassword string `env:"DB_PASSWORD"`
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 5. Security-First-Development-Pattern
|
|
||||||
|
|
||||||
### 5.1 Authentication-Integration
|
|
||||||
|
|
||||||
```go
|
|
||||||
// Jeder Service-Endpunkt bekommt Auth-Context
|
|
||||||
type AuthContext struct {
|
|
||||||
APIKey string
|
|
||||||
Permissions []string
|
|
||||||
ClientIP string
|
|
||||||
UserAgent string
|
|
||||||
RequestID string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) HandleRequest(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// Auth-Context wird vom Gateway gesetzt
|
|
||||||
authCtx := r.Context().Value("auth").(*AuthContext)
|
|
||||||
|
|
||||||
// Service-spezifische Permission-Checks
|
|
||||||
if !authCtx.HasPermission("service:" + s.name + ":write") {
|
|
||||||
http.Error(w, "Forbidden", http.StatusForbidden)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// ... Service-Logik
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5.2 Input-Validation-Standards
|
|
||||||
|
|
||||||
```go
|
|
||||||
// Standard Validation-Middleware
|
|
||||||
func ValidationMiddleware(next http.Handler) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// 1. Content-Type validation
|
|
||||||
if !isValidContentType(r.Header.Get("Content-Type")) {
|
|
||||||
http.Error(w, "Invalid Content-Type", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Content-Length limits
|
|
||||||
if r.ContentLength > MaxRequestSize {
|
|
||||||
http.Error(w, "Request too large", http.StatusRequestEntityTooLarge)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Request-Body validation (service-specific)
|
|
||||||
if err := validateRequestBody(r); err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
next.ServeHTTP(w, r)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5.3 Rate-Limiting-Strategy
|
|
||||||
|
|
||||||
```go
|
|
||||||
// Service-spezifische Rate-Limits
|
|
||||||
type RateLimitConfig struct {
|
|
||||||
RequestsPerMinute int `yaml:"requests_per_minute"`
|
|
||||||
BurstSize int `yaml:"burst_size"`
|
|
||||||
PerAPIKey bool `yaml:"per_api_key"`
|
|
||||||
PerIP bool `yaml:"per_ip"`
|
|
||||||
Whitelist []string `yaml:"whitelist"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Gateway-Level Rate-Limiting
|
|
||||||
func (g *Gateway) GetRateLimit(serviceName, apiKey, clientIP string) *RateLimit {
|
|
||||||
config := g.getRateLimitConfig(serviceName)
|
|
||||||
key := buildRateLimitKey(config, apiKey, clientIP)
|
|
||||||
return g.rateLimiter.GetLimit(key)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 6. Breaking-Change-Management
|
|
||||||
|
|
||||||
### 6.1 API-Versionierung-Strategy
|
|
||||||
|
|
||||||
```go
|
|
||||||
// URL-basierte Versionierung
|
|
||||||
// /v1/mail/send → formular2mail v1
|
|
||||||
// /v2/mail/send → formular2mail v2
|
|
||||||
|
|
||||||
// Gateway-Routing für mehrere Versionen
|
|
||||||
services:
|
|
||||||
formular2mail-v1:
|
|
||||||
path_prefix: "/v1/mail"
|
|
||||||
upstream: "http://127.0.0.1:8081"
|
|
||||||
formular2mail-v2:
|
|
||||||
path_prefix: "/v2/mail"
|
|
||||||
upstream: "http://127.0.0.1:8084"
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6.2 Backward-Compatibility-Pattern
|
|
||||||
|
|
||||||
```go
|
|
||||||
// Service unterstützt mehrere API-Versionen
|
|
||||||
func (s *FormularService) HandleRequest(w http.ResponseWriter, r *http.Request) {
|
|
||||||
version := extractAPIVersion(r.URL.Path) // v1, v2
|
|
||||||
|
|
||||||
switch version {
|
|
||||||
case "v1":
|
|
||||||
s.handleV1Request(w, r)
|
|
||||||
case "v2":
|
|
||||||
s.handleV2Request(w, r)
|
|
||||||
default:
|
|
||||||
s.handleLatestRequest(w, r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6.3 Migration-Pattern
|
|
||||||
|
|
||||||
```go
|
|
||||||
// Coordinated Service-Migration
|
|
||||||
type MigrationPlan struct {
|
|
||||||
FromVersion string
|
|
||||||
ToVersion string
|
|
||||||
Steps []MigrationStep
|
|
||||||
}
|
|
||||||
|
|
||||||
type MigrationStep struct {
|
|
||||||
Name string
|
|
||||||
Component string // "gateway" | "service"
|
|
||||||
Action string // "deploy" | "config" | "test"
|
|
||||||
Rollback func() error
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 7. Checkliste für API-Gateway-Entwicklung
|
|
||||||
|
|
||||||
### 7.1 Vor Implementierungsbeginn
|
|
||||||
|
|
||||||
- [ ] **Service-Impact analysiert:** Welche Services sind betroffen?
|
|
||||||
- [ ] **API-Contract definiert:** OpenAPI-Spec erstellt/aktualisiert?
|
|
||||||
- [ ] **Gateway-Integration geplant:** Routing, Auth, Rate-Limiting?
|
|
||||||
- [ ] **Config-Strategy festgelegt:** Neue Parameter dokumentiert?
|
|
||||||
- [ ] **Breaking-Change-Assessment:** Versionierung erforderlich?
|
|
||||||
- [ ] **Security-Requirements:** Auth, Validation, Rate-Limiting?
|
|
||||||
- [ ] **Test-Strategy:** Unit, Integration, API-Tests geplant?
|
|
||||||
|
|
||||||
### 7.2 Während der Implementierung
|
|
||||||
|
|
||||||
- [ ] **Service-Interface-Compliance:** Standard-Interface implementiert?
|
|
||||||
- [ ] **Error-Handling-Consistency:** Standard-Error-Format verwendet?
|
|
||||||
- [ ] **Health-Check-Integration:** Standardisierte Health-Endpoint?
|
|
||||||
- [ ] **Logging-Standards:** Strukturierte Logs mit Request-IDs?
|
|
||||||
- [ ] **Config-Validation:** Startup-Config-Checks implementiert?
|
|
||||||
- [ ] **Auth-Integration:** Gateway-Auth-Context respektiert?
|
|
||||||
- [ ] **Documentation-Update:** API-Docs und Service-Docs aktualisiert?
|
|
||||||
|
|
||||||
### 7.3 Nach Implementierungsabschluss
|
|
||||||
|
|
||||||
- [ ] **Integration-Tests:** Gateway ↔ Service-Tests bestehen?
|
|
||||||
- [ ] **API-Contract-Tests:** OpenAPI-Compliance validiert?
|
|
||||||
- [ ] **Performance-Tests:** Load-Tests mit Gateway durchgeführt?
|
|
||||||
- [ ] **Security-Tests:** Auth, Input-Validation, Rate-Limiting getestet?
|
|
||||||
- [ ] **Deployment-Scripts:** Service-Deployment automatisiert?
|
|
||||||
- [ ] **Monitoring-Integration:** Health-Checks und Metriken?
|
|
||||||
- [ ] **Documentation-Complete:** Service-Integration dokumentiert?
|
|
||||||
|
|
||||||
## 8. Troubleshooting-Pattern
|
|
||||||
|
|
||||||
### 8.1 Service-Integration-Debugging
|
|
||||||
|
|
||||||
```go
|
|
||||||
// Standard Debug-Endpoints für Services
|
|
||||||
func (s *Service) RegisterDebugEndpoints() {
|
|
||||||
http.HandleFunc("/debug/config", s.debugConfig)
|
|
||||||
http.HandleFunc("/debug/health-detail", s.debugHealthDetail)
|
|
||||||
http.HandleFunc("/debug/metrics", s.debugMetrics)
|
|
||||||
http.HandleFunc("/debug/auth", s.debugAuth)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Gateway Debug-Endpoints
|
|
||||||
func (g *Gateway) RegisterDebugEndpoints() {
|
|
||||||
http.HandleFunc("/debug/services", g.debugServices)
|
|
||||||
http.HandleFunc("/debug/routing", g.debugRouting)
|
|
||||||
http.HandleFunc("/debug/auth-keys", g.debugAuthKeys)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 8.2 Request-Tracing
|
|
||||||
|
|
||||||
```go
|
|
||||||
// Request-ID-Propagation durch alle Services
|
|
||||||
func (g *Gateway) addRequestID(r *http.Request) *http.Request {
|
|
||||||
requestID := r.Header.Get("X-Request-ID")
|
|
||||||
if requestID == "" {
|
|
||||||
requestID = generateRequestID()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Für Service-Weiterleitung
|
|
||||||
r.Header.Set("X-Request-ID", requestID)
|
|
||||||
|
|
||||||
// Für Logging
|
|
||||||
ctx := context.WithValue(r.Context(), "request_id", requestID)
|
|
||||||
return r.WithContext(ctx)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 9. Zusammenfassung: API-Gateway-Entwicklungs-Goldene-Regeln
|
|
||||||
|
|
||||||
1. **API-Contract-First:** OpenAPI-Spec vor Code-Implementation
|
|
||||||
2. **Service-Integration-Aware:** Jede Änderung auf Service-Impact prüfen
|
|
||||||
3. **Security-by-Default:** Auth, Validation, Rate-Limiting bei jedem Endpunkt
|
|
||||||
4. **Configuration-Hierarchie:** Defaults → Environment → Service-specific
|
|
||||||
5. **Multi-Layer-Testing:** Unit → Integration → API → E2E
|
|
||||||
6. **Breaking-Change-Coordination:** Versionierung und Migration planen
|
|
||||||
7. **Health-Check-Integration:** Jeder Service braucht standardisierten Health-Endpoint
|
|
||||||
8. **Request-Tracing:** Request-IDs durch gesamte Pipeline propagieren
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Wichtiger Hinweis:** Diese Entwicklungs-Pattern sind spezifisch für das Furt API-Gateway-System optimiert und sollten bei jeder Entwicklungsaufgabe konsultiert werden, um konsistente und gut integrierte Services zu gewährleisten.
|
|
||||||
|
|
@ -1,673 +0,0 @@
|
||||||
# Furt: API-Gateway im Einklang mit digitaler Souveränität
|
|
||||||
|
|
||||||
**Erstellt:** 03. Juni 2025
|
|
||||||
**Letzte Aktualisierung:** 17. Juni 2025
|
|
||||||
**Version:** 2.0
|
|
||||||
**Verantwortlich:** DAW-Team
|
|
||||||
**Dateipfad:** devdocs/KONZEPT.md
|
|
||||||
|
|
||||||
## Zweck dieses Dokuments
|
|
||||||
|
|
||||||
Dieses Dokument definiert das überarbeitete Konzept für Furt basierend auf der Dragons@Work Tech-Reference und dem Prinzip der maximalen technologischen Souveränität. Das System wird von Go auf C + Lua umgestellt, um Corporate-Capture zu vermeiden.
|
|
||||||
|
|
||||||
## Grund für die Überarbeitung
|
|
||||||
|
|
||||||
Die ursprüngliche Go-basierte Architektur widerspricht den Prinzipien digitaler Souveränität:
|
|
||||||
- **Go ist Google-controlled** (Proxy, Module-Summen, Telemetrie)
|
|
||||||
- **Corporate-Abhängigkeit** durch Go-Ökosystem
|
|
||||||
- **Nicht im Einklang** mit der Tech-Reference
|
|
||||||
|
|
||||||
## 1. Neue Projektvision und Philosophie
|
|
||||||
|
|
||||||
Furt (germanisch für "Durchgang durch Wasser") ist ein **selbst-gehostetes API-Gateway-System** basierend auf **C + Lua**, das vollständig im Einklang mit den Prinzipien digitaler Souveränität steht:
|
|
||||||
|
|
||||||
### Kernprinzipien
|
|
||||||
- **C als Basis** - Bewährt seit 50+ Jahren, strukturell uncapturable
|
|
||||||
- **Lua für Logik** - PUC-Rio University, echte akademische Unabhängigkeit
|
|
||||||
- **Minimale Dependencies** - Nur GCC + musl, keine Corporate-Libraries
|
|
||||||
- **Maximale Transparenz** - Jede Zeile Code verstehbar und kontrollierbar
|
|
||||||
- **Ultra-Low-Tech** - Ressourcenschonend, lange Lebensdauer
|
|
||||||
|
|
||||||
### Abgrenzung zu Corporate-Lösungen
|
|
||||||
- **Keine Cloud-Dependencies**
|
|
||||||
- **Keine Corporate-Runtimes** (Go, Node.js, etc.)
|
|
||||||
- **Keine Black-Box-Components**
|
|
||||||
- **Keine Vendor-Lock-ins**
|
|
||||||
|
|
||||||
## 2. Technische Architektur (C + Lua)
|
|
||||||
|
|
||||||
### 2.1 Technology-Stack
|
|
||||||
|
|
||||||
**Core:**
|
|
||||||
- **C (GCC + musl)** - Gateway-Kern, HTTP-Server, System-Interface
|
|
||||||
- **Lua 5.4** - Business-Logic, Konfiguration, Service-Scripts
|
|
||||||
- **LMDB** - Datenbank (Howard Chu/Symas, keine VC-Dependencies)
|
|
||||||
- **OpenBSD httpd** - Als Reverse-Proxy (langfristig, Apache-Migration)
|
|
||||||
|
|
||||||
**Konfiguration:**
|
|
||||||
- **Lua-Scripts** statt YAML (native, programmierbar, validierbar)
|
|
||||||
- **LMDB-Storage** für Konfiguration und State
|
|
||||||
- **Environment-Variables** nur für Secrets
|
|
||||||
|
|
||||||
**Services:**
|
|
||||||
- **C-Module** mit Lua-Scripten für Business-Logic
|
|
||||||
- **Minimale HTTP-Implementation** in C
|
|
||||||
- **Lua-basierte** Request-Handler
|
|
||||||
|
|
||||||
### 2.2 Projektstruktur
|
|
||||||
|
|
||||||
```
|
|
||||||
furt/
|
|
||||||
├── src/
|
|
||||||
│ ├── core/ # C Core-Implementation
|
|
||||||
│ │ ├── http_server.c # Minimal HTTP-Server
|
|
||||||
│ │ ├── router.c # Request-Routing
|
|
||||||
│ │ ├── proxy.c # Service-Proxy
|
|
||||||
│ │ ├── auth.c # Authentifizierung
|
|
||||||
│ │ └── main.c # Entry Point
|
|
||||||
│ ├── lua/ # Lua-Scripts
|
|
||||||
│ │ ├── gateway/ # Gateway-Logic
|
|
||||||
│ │ │ ├── config.lua # Konfigurationsmanagement
|
|
||||||
│ │ │ ├── middleware.lua # Middleware-Chain
|
|
||||||
│ │ │ └── services.lua # Service-Registry
|
|
||||||
│ │ └── services/ # Service-Logic
|
|
||||||
│ │ ├── formular2mail.lua # Mail-Service
|
|
||||||
│ │ └── sagjan.lua # Comment-Service
|
|
||||||
│ └── include/ # C Header-Files
|
|
||||||
│ ├── furt.h # Main Header
|
|
||||||
│ ├── http.h # HTTP Definitions
|
|
||||||
│ └── lua_bridge.h # C ↔ Lua Interface
|
|
||||||
├── config/ # Konfiguration
|
|
||||||
│ ├── gateway.lua # Gateway-Konfiguration
|
|
||||||
│ └── services/ # Service-Configs
|
|
||||||
│ ├── formular2mail.lua
|
|
||||||
│ └── sagjan.lua
|
|
||||||
├── build/ # Build-System
|
|
||||||
│ ├── Makefile # Haupt-Makefile
|
|
||||||
│ ├── config.mk # Build-Konfiguration
|
|
||||||
│ └── deps/ # Dependencies (Lua, LMDB)
|
|
||||||
├── docs/ # Dokumentation
|
|
||||||
│ ├── installation.md # Installation
|
|
||||||
│ ├── configuration.md # Konfiguration
|
|
||||||
│ └── development.md # Entwicklung
|
|
||||||
├── scripts/ # Build & Deployment
|
|
||||||
│ ├── build.sh # Build-Script
|
|
||||||
│ ├── install.sh # Installation
|
|
||||||
│ └── service-generator.sh # Service-Generator
|
|
||||||
├── tests/ # Tests
|
|
||||||
│ ├── unit/ # Unit-Tests (C)
|
|
||||||
│ ├── integration/ # Integration-Tests
|
|
||||||
│ └── lua/ # Lua-Tests
|
|
||||||
└── examples/ # Beispiele
|
|
||||||
├── hugo/ # Hugo-Integration
|
|
||||||
└── configs/ # Beispiel-Konfigurationen
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2.3 C + Lua Integration-Pattern
|
|
||||||
|
|
||||||
**C-Kern mit Lua-Logic:**
|
|
||||||
```c
|
|
||||||
// src/core/main.c
|
|
||||||
#include "furt.h"
|
|
||||||
#include "lua_bridge.h"
|
|
||||||
|
|
||||||
int main(int argc, char *argv[]) {
|
|
||||||
furt_context_t *ctx = furt_init();
|
|
||||||
|
|
||||||
// Initialize Lua state
|
|
||||||
lua_State *L = lua_bridge_init(ctx);
|
|
||||||
|
|
||||||
// Load gateway configuration
|
|
||||||
lua_bridge_load_config(L, "config/gateway.lua");
|
|
||||||
|
|
||||||
// Start HTTP server
|
|
||||||
http_server_start(ctx, L);
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Lua-Service-Logic:**
|
|
||||||
```lua
|
|
||||||
-- config/services/formular2mail.lua
|
|
||||||
local formular2mail = {
|
|
||||||
name = "formular2mail",
|
|
||||||
path_prefix = "/v1/mail",
|
|
||||||
port = 8081,
|
|
||||||
|
|
||||||
-- Request-Handler
|
|
||||||
handle_request = function(request)
|
|
||||||
if request.method ~= "POST" then
|
|
||||||
return { status = 405, body = "Method not allowed" }
|
|
||||||
end
|
|
||||||
|
|
||||||
local data = json.decode(request.body)
|
|
||||||
if not validate_mail_data(data) then
|
|
||||||
return { status = 400, body = "Invalid data" }
|
|
||||||
end
|
|
||||||
|
|
||||||
local result = send_mail(data)
|
|
||||||
return { status = 200, body = json.encode(result) }
|
|
||||||
end,
|
|
||||||
|
|
||||||
-- Health-Check
|
|
||||||
health_check = function()
|
|
||||||
return { status = "healthy", timestamp = os.time() }
|
|
||||||
end
|
|
||||||
}
|
|
||||||
|
|
||||||
return formular2mail
|
|
||||||
```
|
|
||||||
|
|
||||||
## 3. Build-System und Dependencies
|
|
||||||
|
|
||||||
### 3.1 Minimale Dependencies
|
|
||||||
|
|
||||||
**Required:**
|
|
||||||
- **GCC** (oder clang, aber nicht MSVC)
|
|
||||||
- **musl libc** (vermeidet glibc-Komplexität)
|
|
||||||
- **Lua 5.4** (als Source eingebunden, nicht als Package)
|
|
||||||
- **LMDB** (als Source eingebunden)
|
|
||||||
|
|
||||||
**Optional:**
|
|
||||||
- **Valgrind** für Memory-Debugging
|
|
||||||
- **strace** für System-Call-Debugging
|
|
||||||
|
|
||||||
### 3.2 Build-Process
|
|
||||||
|
|
||||||
```makefile
|
|
||||||
# build/Makefile
|
|
||||||
CC = gcc
|
|
||||||
CFLAGS = -std=c99 -Wall -Wextra -O2 -DLUA_USE_POSIX
|
|
||||||
LDFLAGS = -lm -ldl
|
|
||||||
|
|
||||||
# Dependencies
|
|
||||||
LUA_DIR = deps/lua-5.4.6
|
|
||||||
LMDB_DIR = deps/lmdb
|
|
||||||
|
|
||||||
SOURCES = src/core/*.c $(LUA_DIR)/src/*.c $(LMDB_DIR)/*.c
|
|
||||||
TARGET = bin/furt-gateway
|
|
||||||
|
|
||||||
all: $(TARGET)
|
|
||||||
|
|
||||||
$(TARGET): $(SOURCES)
|
|
||||||
$(CC) $(CFLAGS) -I$(LUA_DIR)/src -I$(LMDB_DIR) \
|
|
||||||
$(SOURCES) -o $(TARGET) $(LDFLAGS)
|
|
||||||
|
|
||||||
clean:
|
|
||||||
rm -f $(TARGET)
|
|
||||||
|
|
||||||
install: $(TARGET)
|
|
||||||
cp $(TARGET) /usr/local/bin/
|
|
||||||
cp -r config/ /etc/furt/
|
|
||||||
cp -r src/lua/ /usr/local/share/furt/
|
|
||||||
|
|
||||||
.PHONY: all clean install
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3.3 Dependency-Management
|
|
||||||
|
|
||||||
**Statically Linked:**
|
|
||||||
- Lua-Source direkt eingebunden
|
|
||||||
- LMDB-Source direkt eingebunden
|
|
||||||
- Keine Package-Manager-Dependencies
|
|
||||||
|
|
||||||
**Why Static Linking:**
|
|
||||||
- Vermeidet Library-Version-Konflikte
|
|
||||||
- Reduziert Runtime-Dependencies
|
|
||||||
- Maximale Kontrolle über Code-Path
|
|
||||||
|
|
||||||
## 4. Service-Entwicklung-Pattern
|
|
||||||
|
|
||||||
### 4.1 Service als C-Module + Lua-Script
|
|
||||||
|
|
||||||
**Service-Interface in C:**
|
|
||||||
```c
|
|
||||||
// src/core/service.h
|
|
||||||
typedef struct {
|
|
||||||
char *name;
|
|
||||||
char *path_prefix;
|
|
||||||
int port;
|
|
||||||
lua_State *lua_state;
|
|
||||||
} furt_service_t;
|
|
||||||
|
|
||||||
// Service-Lifecycle
|
|
||||||
int service_init(furt_service_t *service, const char *config_path);
|
|
||||||
int service_handle_request(furt_service_t *service, http_request_t *req, http_response_t *res);
|
|
||||||
int service_health_check(furt_service_t *service);
|
|
||||||
void service_cleanup(furt_service_t *service);
|
|
||||||
```
|
|
||||||
|
|
||||||
**Service-Logic in Lua:**
|
|
||||||
```lua
|
|
||||||
-- src/lua/services/base.lua
|
|
||||||
local base_service = {
|
|
||||||
-- Standard Request-Verarbeitung
|
|
||||||
process_request = function(self, request)
|
|
||||||
-- Input-Validation
|
|
||||||
if not self:validate_input(request) then
|
|
||||||
return self:error_response(400, "Invalid input")
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Business-Logic
|
|
||||||
local result = self:handle_business_logic(request)
|
|
||||||
|
|
||||||
-- Response-Formatting
|
|
||||||
return self:format_response(result)
|
|
||||||
end,
|
|
||||||
|
|
||||||
-- Standard Error-Handling
|
|
||||||
error_response = function(self, status, message)
|
|
||||||
return {
|
|
||||||
status = status,
|
|
||||||
headers = { ["Content-Type"] = "application/json" },
|
|
||||||
body = json.encode({ error = message })
|
|
||||||
}
|
|
||||||
end
|
|
||||||
}
|
|
||||||
|
|
||||||
return base_service
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4.2 Service-Generator (Shell + Lua)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
#!/bin/bash
|
|
||||||
# scripts/service-generator.sh
|
|
||||||
|
|
||||||
SERVICE_NAME=$1
|
|
||||||
if [ -z "$SERVICE_NAME" ]; then
|
|
||||||
echo "Usage: $0 <service_name>"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Create service Lua file
|
|
||||||
cat > "src/lua/services/${SERVICE_NAME}.lua" << EOF
|
|
||||||
local base = require("services.base")
|
|
||||||
local ${SERVICE_NAME} = {}
|
|
||||||
|
|
||||||
setmetatable(${SERVICE_NAME}, { __index = base })
|
|
||||||
|
|
||||||
function ${SERVICE_NAME}:handle_business_logic(request)
|
|
||||||
-- TODO: Implement ${SERVICE_NAME} logic
|
|
||||||
return { message = "Hello from ${SERVICE_NAME}" }
|
|
||||||
end
|
|
||||||
|
|
||||||
function ${SERVICE_NAME}:validate_input(request)
|
|
||||||
-- TODO: Implement validation
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
|
|
||||||
return ${SERVICE_NAME}
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# Create config file
|
|
||||||
cat > "config/services/${SERVICE_NAME}.lua" << EOF
|
|
||||||
return {
|
|
||||||
name = "${SERVICE_NAME}",
|
|
||||||
path_prefix = "/v1/${SERVICE_NAME}",
|
|
||||||
port = 808X, -- TODO: Set port
|
|
||||||
enabled = true,
|
|
||||||
|
|
||||||
-- Service-specific config
|
|
||||||
config = {
|
|
||||||
-- TODO: Add service configuration
|
|
||||||
}
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
|
|
||||||
echo "Created service: ${SERVICE_NAME}"
|
|
||||||
```
|
|
||||||
|
|
||||||
## 5. Konfigurationsmanagement (Lua-basiert)
|
|
||||||
|
|
||||||
### 5.1 Lua-Konfiguration statt YAML
|
|
||||||
|
|
||||||
**Gateway-Konfiguration:**
|
|
||||||
```lua
|
|
||||||
-- config/gateway.lua
|
|
||||||
return {
|
|
||||||
server = {
|
|
||||||
host = "127.0.0.1",
|
|
||||||
port = 8080,
|
|
||||||
max_connections = 1000,
|
|
||||||
request_timeout = 30
|
|
||||||
},
|
|
||||||
|
|
||||||
security = {
|
|
||||||
api_keys = {
|
|
||||||
{
|
|
||||||
key = os.getenv("HUGO_API_KEY"),
|
|
||||||
name = "hugo-frontend",
|
|
||||||
permissions = { "mail:send", "comments:read" },
|
|
||||||
allowed_ips = { "127.0.0.1", "10.0.0.0/8" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
services = {
|
|
||||||
require("services.formular2mail"),
|
|
||||||
require("services.sagjan")
|
|
||||||
},
|
|
||||||
|
|
||||||
logging = {
|
|
||||||
level = "info",
|
|
||||||
file = "/var/log/furt/gateway.log"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Vorteile Lua-Config:**
|
|
||||||
- **Native Programmierbarkeit** (Loops, Conditions, Functions)
|
|
||||||
- **Environment-Variable-Access** direkt in Config
|
|
||||||
- **Validation** durch Lua-Logic möglich
|
|
||||||
- **Kommentare und Dokumentation** integriert
|
|
||||||
- **Modularität** durch require()
|
|
||||||
|
|
||||||
### 5.2 Config-Validation
|
|
||||||
|
|
||||||
```lua
|
|
||||||
-- src/lua/gateway/config_validator.lua
|
|
||||||
local validator = {}
|
|
||||||
|
|
||||||
function validator.validate_gateway_config(config)
|
|
||||||
assert(config.server, "server config required")
|
|
||||||
assert(config.server.port, "server.port required")
|
|
||||||
assert(type(config.server.port) == "number", "server.port must be number")
|
|
||||||
|
|
||||||
for _, service in ipairs(config.services) do
|
|
||||||
validator.validate_service_config(service)
|
|
||||||
end
|
|
||||||
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
|
|
||||||
function validator.validate_service_config(service)
|
|
||||||
assert(service.name, "service.name required")
|
|
||||||
assert(service.path_prefix, "service.path_prefix required")
|
|
||||||
assert(service.port, "service.port required")
|
|
||||||
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
|
|
||||||
return validator
|
|
||||||
```
|
|
||||||
|
|
||||||
## 6. Authentifizierung und Sicherheit
|
|
||||||
|
|
||||||
### 6.1 C-basierte Auth-Implementation
|
|
||||||
|
|
||||||
```c
|
|
||||||
// src/core/auth.c
|
|
||||||
#include "auth.h"
|
|
||||||
#include <string.h>
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
char key[64];
|
|
||||||
char name[32];
|
|
||||||
char **permissions;
|
|
||||||
char **allowed_ips;
|
|
||||||
} api_key_t;
|
|
||||||
|
|
||||||
int auth_validate_api_key(const char *key, const char *client_ip) {
|
|
||||||
// Load API keys from Lua config
|
|
||||||
// Validate key and IP
|
|
||||||
// Return permissions mask
|
|
||||||
}
|
|
||||||
|
|
||||||
int auth_check_permission(int permissions_mask, const char *permission) {
|
|
||||||
// Check if permission is allowed
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6.2 IP-Allowlisting in C
|
|
||||||
|
|
||||||
```c
|
|
||||||
// src/core/ip_filter.c
|
|
||||||
#include <arpa/inet.h>
|
|
||||||
|
|
||||||
int ip_is_allowed(const char *client_ip, char **allowed_ips) {
|
|
||||||
for (int i = 0; allowed_ips[i] != NULL; i++) {
|
|
||||||
if (ip_matches_cidr(client_ip, allowed_ips[i])) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int ip_matches_cidr(const char *ip, const char *cidr) {
|
|
||||||
// CIDR matching implementation
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 7. Performance und Ressourcen
|
|
||||||
|
|
||||||
### 7.1 Memory-Management
|
|
||||||
|
|
||||||
**C-Memory-Management:**
|
|
||||||
- **Stack-Allocation** wo möglich
|
|
||||||
- **Explicit malloc/free** für dynamische Allocations
|
|
||||||
- **Memory-Pools** für häufige Allocations
|
|
||||||
- **Valgrind-Testing** mandatory
|
|
||||||
|
|
||||||
**Lua-Integration:**
|
|
||||||
- **Lua-GC-Tuning** für Server-Workloads
|
|
||||||
- **C ↔ Lua** Memory-Boundary klar definiert
|
|
||||||
- **Lua-State-Isolation** zwischen Services
|
|
||||||
|
|
||||||
### 7.2 Performance-Charakteristika
|
|
||||||
|
|
||||||
**Erwartete Performance:**
|
|
||||||
- **Memory:** < 10 MB für Gateway + 3 Services
|
|
||||||
- **CPU:** < 1% bei 100 req/s
|
|
||||||
- **Startup:** < 100ms Cold-Start
|
|
||||||
- **Latency:** < 1ms Gateway-Overhead
|
|
||||||
|
|
||||||
**vs. Go-Implementation:**
|
|
||||||
- **10x weniger Memory** (keine GC, kein Runtime)
|
|
||||||
- **5x weniger CPU** (native Code, keine Abstractions)
|
|
||||||
- **100x schneller Startup** (kein Runtime-Init)
|
|
||||||
|
|
||||||
## 8. Testing-Strategy
|
|
||||||
|
|
||||||
### 8.1 C-Code-Testing
|
|
||||||
|
|
||||||
```c
|
|
||||||
// tests/unit/test_auth.c
|
|
||||||
#include "auth.h"
|
|
||||||
#include "test_framework.h"
|
|
||||||
|
|
||||||
void test_api_key_validation() {
|
|
||||||
// Setup
|
|
||||||
auth_init_test_keys();
|
|
||||||
|
|
||||||
// Test valid key
|
|
||||||
assert_true(auth_validate_api_key("valid-key", "127.0.0.1"));
|
|
||||||
|
|
||||||
// Test invalid key
|
|
||||||
assert_false(auth_validate_api_key("invalid-key", "127.0.0.1"));
|
|
||||||
|
|
||||||
// Test IP restriction
|
|
||||||
assert_false(auth_validate_api_key("valid-key", "192.168.1.1"));
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 8.2 Lua-Logic-Testing
|
|
||||||
|
|
||||||
```lua
|
|
||||||
-- tests/lua/test_services.lua
|
|
||||||
local service = require("services.formular2mail")
|
|
||||||
|
|
||||||
function test_mail_service_validation()
|
|
||||||
local request = {
|
|
||||||
method = "POST",
|
|
||||||
body = '{"name":"Test","email":"test@example.com","message":"Test"}'
|
|
||||||
}
|
|
||||||
|
|
||||||
local response = service:process_request(request)
|
|
||||||
assert(response.status == 200)
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
### 8.3 Integration-Testing
|
|
||||||
|
|
||||||
```bash
|
|
||||||
#!/bin/bash
|
|
||||||
# tests/integration/test_gateway.sh
|
|
||||||
|
|
||||||
# Start test gateway
|
|
||||||
./bin/furt-gateway &
|
|
||||||
GATEWAY_PID=$!
|
|
||||||
|
|
||||||
# Test API endpoint
|
|
||||||
curl -X POST http://localhost:8080/v1/mail/send \
|
|
||||||
-H "X-API-Key: test-key" \
|
|
||||||
-d '{"name":"Test","email":"test@example.com","message":"Test"}'
|
|
||||||
|
|
||||||
# Cleanup
|
|
||||||
kill $GATEWAY_PID
|
|
||||||
```
|
|
||||||
|
|
||||||
## 9. Deployment und Installation
|
|
||||||
|
|
||||||
### 9.1 Native Installation (Single Binary)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Build from source
|
|
||||||
git clone https://gitea.dragons-at-work.de/DAW/furt.git
|
|
||||||
cd furt
|
|
||||||
make clean all
|
|
||||||
|
|
||||||
# Install
|
|
||||||
sudo make install
|
|
||||||
|
|
||||||
# Configure
|
|
||||||
sudo cp examples/configs/* /etc/furt/
|
|
||||||
|
|
||||||
# Start
|
|
||||||
sudo systemctl enable furt-gateway
|
|
||||||
sudo systemctl start furt-gateway
|
|
||||||
```
|
|
||||||
|
|
||||||
### 9.2 Systemd-Integration
|
|
||||||
|
|
||||||
```ini
|
|
||||||
# /etc/systemd/system/furt-gateway.service
|
|
||||||
[Unit]
|
|
||||||
Description=Furt API Gateway
|
|
||||||
After=network.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=simple
|
|
||||||
User=furt
|
|
||||||
Group=furt
|
|
||||||
ExecStart=/usr/local/bin/furt-gateway
|
|
||||||
Restart=always
|
|
||||||
RestartSec=5
|
|
||||||
StandardOutput=journal
|
|
||||||
StandardError=journal
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
```
|
|
||||||
|
|
||||||
### 9.3 OpenBSD-Integration (langfristig)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# /etc/rc.d/furt_gateway
|
|
||||||
#!/bin/ksh
|
|
||||||
daemon="/usr/local/bin/furt-gateway"
|
|
||||||
daemon_user="_furt"
|
|
||||||
|
|
||||||
. /etc/rc.d/rc.subr
|
|
||||||
|
|
||||||
rc_cmd $1
|
|
||||||
```
|
|
||||||
|
|
||||||
## 10. Migration von Go zu C + Lua
|
|
||||||
|
|
||||||
### 10.1 Migration-Strategy
|
|
||||||
|
|
||||||
**Phase 1: C-Grundgerüst**
|
|
||||||
- HTTP-Server in C implementieren
|
|
||||||
- Basis-Routing implementieren
|
|
||||||
- Lua-Integration testen
|
|
||||||
|
|
||||||
**Phase 2: Service-Migration**
|
|
||||||
- Formular2Mail-Service als Lua-Script
|
|
||||||
- Auth-System in C + Lua
|
|
||||||
- Tests migrieren
|
|
||||||
|
|
||||||
**Phase 3: Feature-Parität**
|
|
||||||
- Alle Go-Features in C + Lua
|
|
||||||
- Performance-Optimierung
|
|
||||||
- Dokumentation-Update
|
|
||||||
|
|
||||||
**Phase 4: Deployment**
|
|
||||||
- Production-Deployment
|
|
||||||
- Go-Version deprecaten
|
|
||||||
- Community-Feedback integrieren
|
|
||||||
|
|
||||||
### 10.2 Kompatibilität
|
|
||||||
|
|
||||||
**API-Compatibility:**
|
|
||||||
- Gleiche HTTP-APIs wie Go-Version
|
|
||||||
- Gleiche Konfiguration-Semantik (aber Lua statt YAML)
|
|
||||||
- Gleiche Integration mit Hugo-Shortcodes
|
|
||||||
|
|
||||||
**Migration-Pfad:**
|
|
||||||
- Side-by-Side-Deployment möglich
|
|
||||||
- Graduelle Service-Migration
|
|
||||||
- Zero-Downtime-Umstellung
|
|
||||||
|
|
||||||
## 11. Langfristige Vision
|
|
||||||
|
|
||||||
### 11.1 Souveräne Technologie-Stack
|
|
||||||
|
|
||||||
**Complete Independence:**
|
|
||||||
- **Compiler:** GCC (GNU, nicht Corporate)
|
|
||||||
- **Libc:** musl (minimal, secure)
|
|
||||||
- **Database:** LMDB (academic, proven)
|
|
||||||
- **Scripting:** Lua (university-backed)
|
|
||||||
- **HTTP-Server:** Eigene Implementation (< 1000 Zeilen C)
|
|
||||||
|
|
||||||
### 11.2 Community und Open Source
|
|
||||||
|
|
||||||
**Authentic Open Source:**
|
|
||||||
- Apache 2.0 License
|
|
||||||
- Keine Corporate-Contributors
|
|
||||||
- Community-driven Development
|
|
||||||
- Educational Documentation
|
|
||||||
|
|
||||||
**Biocodie-Integration:**
|
|
||||||
- Furt als Referenz-Implementation für "Organische Software"
|
|
||||||
- Minimale Komplexität, maximale Transparenz
|
|
||||||
- Natürliche Wachstums-Pattern
|
|
||||||
|
|
||||||
## 12. Nächste Schritte
|
|
||||||
|
|
||||||
### 12.1 Unmittelbare Implementierung
|
|
||||||
|
|
||||||
1. **C-HTTP-Server** - Minimal-Implementation (< 500 Zeilen)
|
|
||||||
2. **Lua-Integration** - C ↔ Lua Bridge etablieren
|
|
||||||
3. **Build-System** - Makefile-basiertes Build
|
|
||||||
4. **Basic-Routing** - Request-Routing in C + Lua
|
|
||||||
|
|
||||||
### 12.2 Service-Implementation
|
|
||||||
|
|
||||||
1. **Formular2Mail** als erstes Lua-Service
|
|
||||||
2. **Authentication** in C mit Lua-Config
|
|
||||||
3. **Hugo-Integration** - Shortcodes adaptieren
|
|
||||||
4. **Testing-Framework** - C + Lua Tests
|
|
||||||
|
|
||||||
### 12.3 Production-Readiness
|
|
||||||
|
|
||||||
1. **Performance-Tuning** - Memory, CPU optimieren
|
|
||||||
2. **Security-Hardening** - Input-Validation, Memory-Safety
|
|
||||||
3. **Documentation** - Installation, Configuration, Development
|
|
||||||
4. **Deployment-Automation** - Scripts für Production
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Diese überarbeitete Architektur entspricht vollständig der Dragons@Work Tech-Reference und ermöglicht echte digitale Souveränität durch Corporate-freie Technologien.**
|
|
||||||
|
|
||||||
|
|
@ -1,296 +0,0 @@
|
||||||
# Furt: Master-Strategie & Technologie-Migration
|
|
||||||
|
|
||||||
**Erstellt:** 17. Juni 2025
|
|
||||||
**Letzte Aktualisierung:** 17. Juni 2025
|
|
||||||
**Version:** 1.0
|
|
||||||
**Verantwortlich:** DAW-Team
|
|
||||||
**Dateipfad:** devdocs/MASTER_STRATEGY.md
|
|
||||||
|
|
||||||
## Zweck dieses Dokuments
|
|
||||||
|
|
||||||
Dieses Dokument definiert die langfristige Technologie-Migrationsstrategie für das gesamte Dragons@Work Ökosystem, mit Furt als erstem Schritt zur vollständigen digitalen Souveränität.
|
|
||||||
|
|
||||||
## 🎯 Gesamtvision: Komplette Tech-Souveränität
|
|
||||||
|
|
||||||
### Ausgangssituation (Juni 2025)
|
|
||||||
- **Server:** Ubuntu + Apache + ISPConfig
|
|
||||||
- **Website:** Hugo + digitalindependent Theme
|
|
||||||
- **Planned API:** Go-basiertes Furt (noch nicht implementiert)
|
|
||||||
- **Dependencies:** Viele Corporate-controlled Tools
|
|
||||||
|
|
||||||
### Zielsituation (18-24 Monate)
|
|
||||||
- **Server:** OpenBSD + httpd + eigene Scripts
|
|
||||||
- **Website:** vefari (eigener Generator) + eigenes Theme
|
|
||||||
- **API:** C + Lua Furt (vollständig souverän)
|
|
||||||
- **Dependencies:** Minimal, alle selbst-kontrolliert
|
|
||||||
|
|
||||||
## 📋 Migration-Roadmap
|
|
||||||
|
|
||||||
### Phase 1: Furt API-Grundlagen (4 Wochen)
|
|
||||||
**Woche 1: Mail-Service (Pure Lua)**
|
|
||||||
- [x] Entscheidung: Pure Lua statt Go
|
|
||||||
- [ ] HTTP-Server (lua-socket)
|
|
||||||
- [ ] Mail-Handler (SMTP zu Postfix)
|
|
||||||
- [ ] API-Key-Auth
|
|
||||||
- [ ] Hugo-Integration (POST-based)
|
|
||||||
|
|
||||||
**Woche 2-3: Service-Expansion**
|
|
||||||
- [ ] Comment-Service (sagjan-Integration)
|
|
||||||
- [ ] Health-Checks
|
|
||||||
- [ ] Error-Handling
|
|
||||||
- [ ] Basic Logging
|
|
||||||
|
|
||||||
**Woche 4: Production-Ready**
|
|
||||||
- [ ] HTTPS (Lua-SSL)
|
|
||||||
- [ ] Systemd-Integration
|
|
||||||
- [ ] Monitoring
|
|
||||||
- [ ] Documentation
|
|
||||||
|
|
||||||
### Phase 2: C-Integration (4-6 Wochen)
|
|
||||||
**Performance-Layer:**
|
|
||||||
- [ ] C-HTTP-Server (< 500 Zeilen)
|
|
||||||
- [ ] C ↔ Lua Bridge
|
|
||||||
- [ ] Memory-Management
|
|
||||||
- [ ] Security-Hardening
|
|
||||||
|
|
||||||
### Phase 3: Infrastructure-Migration (6-12 Monate)
|
|
||||||
**Server-Migration:**
|
|
||||||
- [ ] OpenBSD-Evaluation
|
|
||||||
- [ ] ISPConfig → eigene Scripts
|
|
||||||
- [ ] Apache → OpenBSD httpd
|
|
||||||
- [ ] SSL-Management ohne Corporate-Tools
|
|
||||||
|
|
||||||
### Phase 4: Website-Migration (3-6 Monate parallel)
|
|
||||||
**vefari-Entwicklung:**
|
|
||||||
- [ ] Hugo-Kompatibilität (Templates/Content)
|
|
||||||
- [ ] Markdown-Processing
|
|
||||||
- [ ] Multi-Language-Support
|
|
||||||
- [ ] Build-System
|
|
||||||
|
|
||||||
### Phase 5: Complete Independence (langfristig)
|
|
||||||
**Advanced Features:**
|
|
||||||
- [ ] Eigener Browser (Exploration)
|
|
||||||
- [ ] Föderation zwischen Furt-Instanzen
|
|
||||||
- [ ] Advanced Services (Shop, Calendar, etc.)
|
|
||||||
|
|
||||||
## 🏗️ Architektur-Prinzipien
|
|
||||||
|
|
||||||
### Modularität (Anti-Monolith)
|
|
||||||
```
|
|
||||||
Jedes Script/Modul: < 200 Zeilen
|
|
||||||
Jede Funktion: < 50 Zeilen
|
|
||||||
Jede Datei: Ein klarer Zweck
|
|
||||||
Keine 800-Zeilen-Monster!
|
|
||||||
```
|
|
||||||
|
|
||||||
### Technologie-Auswahl nach Tech-Reference
|
|
||||||
✅ **Erlaubt (Souverän):**
|
|
||||||
- C + GCC/musl
|
|
||||||
- Lua (PUC-Rio University)
|
|
||||||
- LMDB (Howard Chu/Symas)
|
|
||||||
- OpenBSD httpd
|
|
||||||
|
|
||||||
❌ **Vermeiden (Corporate-Controlled):**
|
|
||||||
- Go (Google)
|
|
||||||
- Node.js (Corporate-Oligopol)
|
|
||||||
- Apache (Corporate-finanziert)
|
|
||||||
- MariaDB (VC-finanziert)
|
|
||||||
|
|
||||||
⚠️ **Temporary OK (Migration-Pfad):**
|
|
||||||
- Ubuntu (→ OpenBSD)
|
|
||||||
- Apache (→ OpenBSD httpd)
|
|
||||||
|
|
||||||
### Development-Prinzipien
|
|
||||||
1. **Verstehbarkeit** vor Features
|
|
||||||
2. **Kleine Module** vor Monolithen
|
|
||||||
3. **Eigene Kontrolle** vor Convenience
|
|
||||||
4. **Langfristig stabil** vor "Modern"
|
|
||||||
5. **Testing** für alles
|
|
||||||
|
|
||||||
## 🔧 Technical Implementation Strategy
|
|
||||||
|
|
||||||
### Furt-Architecture (Final)
|
|
||||||
```
|
|
||||||
┌─────────────────┐
|
|
||||||
│ OpenBSD httpd │ (SSL-Terminierung)
|
|
||||||
│ (Port 443) │
|
|
||||||
└─────────┬───────┘
|
|
||||||
│
|
|
||||||
┌─────────▼───────┐
|
|
||||||
│ C-HTTP-Gateway │ (Routing, Auth)
|
|
||||||
│ (Port 8080) │
|
|
||||||
└─────────┬───────┘
|
|
||||||
│
|
|
||||||
┌─────▼─────┐ ┌─────────┐ ┌─────────┐
|
|
||||||
│ Lua-Mail │ │Lua-Comm│ │Lua-Shop │
|
|
||||||
│(Port 8081)│ │(8082) │ │(8083) │
|
|
||||||
└───────────┘ └─────────┘ └─────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
### Service-Pattern (Standardisiert)
|
|
||||||
```lua
|
|
||||||
-- services/template.lua
|
|
||||||
local service = {
|
|
||||||
name = "service_name",
|
|
||||||
port = 808X,
|
|
||||||
|
|
||||||
-- Standard Interface
|
|
||||||
handle_request = function(self, request)
|
|
||||||
-- Input-Validation (< 20 Zeilen)
|
|
||||||
-- Business-Logic (< 50 Zeilen)
|
|
||||||
-- Response-Formatting (< 10 Zeilen)
|
|
||||||
end,
|
|
||||||
|
|
||||||
health_check = function(self)
|
|
||||||
-- Health-Logic (< 10 Zeilen)
|
|
||||||
end
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Testing-Strategy
|
|
||||||
```
|
|
||||||
tests/
|
|
||||||
├── unit/ # Lua-Modul-Tests
|
|
||||||
│ ├── test_mail.lua
|
|
||||||
│ └── test_auth.lua
|
|
||||||
├── integration/ # Service-Tests
|
|
||||||
│ └── test_api.lua
|
|
||||||
├── system/ # End-to-End-Tests
|
|
||||||
│ └── test_hugo.lua
|
|
||||||
└── performance/ # Load-Tests
|
|
||||||
└── test_load.lua
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📊 SMTP-Configuration (Week 1)
|
|
||||||
|
|
||||||
### Postfix-Integration
|
|
||||||
```lua
|
|
||||||
-- config/mail.lua
|
|
||||||
return {
|
|
||||||
smtp = {
|
|
||||||
server = "mail.dragons-at-work.de",
|
|
||||||
port = 465,
|
|
||||||
username = os.getenv("MAIL_USERNAME"), -- xxxx@dragons-at-work.de
|
|
||||||
password = os.getenv("MAIL_PASSWORD"),
|
|
||||||
security = "ssl/tls",
|
|
||||||
from = "noreply@dragons-at-work.de",
|
|
||||||
to = "michael@dragons-at-work.de"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Security-Pattern
|
|
||||||
```lua
|
|
||||||
-- Nie Passwörter in Code!
|
|
||||||
-- Environment-Variables für Secrets
|
|
||||||
-- API-Keys in LMDB
|
|
||||||
-- IP-Allowlisting für Hugo
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🚀 Hugo-Integration (Week 1)
|
|
||||||
|
|
||||||
### Shortcode-Pattern
|
|
||||||
```hugo
|
|
||||||
{{< furt-mail
|
|
||||||
api-endpoint="https://api.dragons-at-work.de/v1/mail/send"
|
|
||||||
api-key="hugo-frontend-key"
|
|
||||||
success-url="/contact/thanks/"
|
|
||||||
>}}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Progressive Enhancement
|
|
||||||
```html
|
|
||||||
<!-- Form funktioniert ohne JavaScript -->
|
|
||||||
<form method="POST" action="/v1/mail/send">
|
|
||||||
<input name="name" required>
|
|
||||||
<input name="email" type="email" required>
|
|
||||||
<textarea name="message" required></textarea>
|
|
||||||
<button type="submit">Senden</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<!-- JavaScript für UX-Enhancement -->
|
|
||||||
<script>
|
|
||||||
// AJAX-Submit, aber Fallback auf normale Form
|
|
||||||
</script>
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📈 Success-Metrics
|
|
||||||
|
|
||||||
### Week 1 Success-Criteria
|
|
||||||
- [ ] HTTP-Request funktioniert
|
|
||||||
- [ ] Mail wird via SMTP gesendet
|
|
||||||
- [ ] API-Key-Auth schützt Endpoint
|
|
||||||
- [ ] Hugo-Form sendet erfolgreich
|
|
||||||
- [ ] < 100ms Response-Time
|
|
||||||
- [ ] Jedes Modul < 200 Zeilen
|
|
||||||
|
|
||||||
### Phase 1 Success-Criteria
|
|
||||||
- [ ] Production-ready Mail-Service
|
|
||||||
- [ ] Comment-Service implementiert
|
|
||||||
- [ ] HTTPS mit Lua-SSL
|
|
||||||
- [ ] Systemd-Service läuft stabil
|
|
||||||
- [ ] Documentation komplett
|
|
||||||
|
|
||||||
### Long-term Success-Criteria
|
|
||||||
- [ ] Komplette Ubuntu → OpenBSD Migration
|
|
||||||
- [ ] Hugo → vefari Migration
|
|
||||||
- [ ] < 10 MB Total Memory für alle Services
|
|
||||||
- [ ] Zero Corporate-Dependencies
|
|
||||||
|
|
||||||
## 🔄 Migration-Safety
|
|
||||||
|
|
||||||
### Parallel-Betrieb-Strategie
|
|
||||||
```
|
|
||||||
Week 1-4: Lua-Furt || Apache (parallel)
|
|
||||||
Month 2-6: C+Lua-Furt || Apache (parallel)
|
|
||||||
Month 6+: C+Lua-Furt || OpenBSD-httpd (parallel)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Rollback-Plan
|
|
||||||
- Jede Migration-Phase kann rückgängig gemacht werden
|
|
||||||
- Hugo bleibt funktionsfähig während vefari-Entwicklung
|
|
||||||
- Apache bleibt als Fallback während OpenBSD-Migration
|
|
||||||
|
|
||||||
### Testing vor Production
|
|
||||||
- Alle Changes erst auf Testumgebung
|
|
||||||
- Graduelle Umstellung Service für Service
|
|
||||||
- Monitoring für Performance-Regression
|
|
||||||
|
|
||||||
## 📝 Documentation-Strategy
|
|
||||||
|
|
||||||
### Development-Docs
|
|
||||||
- [ ] **Installation-Guide** (Linux → OpenBSD)
|
|
||||||
- [ ] **API-Documentation** (OpenAPI-Style)
|
|
||||||
- [ ] **Service-Development-Guide** (Lua-Pattern)
|
|
||||||
- [ ] **Testing-Guide** (Unit + Integration)
|
|
||||||
|
|
||||||
### User-Docs
|
|
||||||
- [ ] **Hugo-Integration-Guide**
|
|
||||||
- [ ] **vefari-Migration-Guide**
|
|
||||||
- [ ] **Self-Hosting-Guide**
|
|
||||||
|
|
||||||
### Philosophy-Docs
|
|
||||||
- [ ] **Tech-Souveränität-Rationale**
|
|
||||||
- [ ] **Corporate-Capture-Analysis**
|
|
||||||
- [ ] **Long-term-Vision**
|
|
||||||
|
|
||||||
## 🎯 Next Session Preparation
|
|
||||||
|
|
||||||
### Session-Focus: Lua-HTTP-Server Start
|
|
||||||
1. **Lua-Dependencies** installieren
|
|
||||||
2. **Basic HTTP-Server** (50-100 Zeilen)
|
|
||||||
3. **Request-Parsing** (POST-Body, Headers)
|
|
||||||
4. **Response-Formatting** (JSON)
|
|
||||||
5. **Error-Handling** (Basic)
|
|
||||||
|
|
||||||
### Session-Deliverable
|
|
||||||
- `src/main.lua` - Funktionierender HTTP-Server
|
|
||||||
- `test/test_http.lua` - Basis-Tests
|
|
||||||
- `scripts/start.sh` - Start-Script
|
|
||||||
|
|
||||||
### Session-Success-Metric
|
|
||||||
- `curl -X POST http://localhost:8080/test` → HTTP 200 Response
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Diese Master-Strategie dient als Kompass für alle technischen Entscheidungen und stellt sicher, dass jeder kleine Schritt zum großen Ziel der technologischen Souveränität beiträgt.**
|
|
||||||
|
|
@ -1,783 +0,0 @@
|
||||||
# Furt Testing-Richtlinien
|
|
||||||
|
|
||||||
**Erstellt:** 03.06.2025
|
|
||||||
**Letzte Aktualisierung:** 03.06.2025
|
|
||||||
**Version:** 1.0
|
|
||||||
**Verantwortlich:** DAW-Team
|
|
||||||
**Dateipfad:** devdocs/TESTING_GUIDELINES.md
|
|
||||||
|
|
||||||
## Zweck dieses Dokuments
|
|
||||||
|
|
||||||
Dieses Dokument definiert verbindliche Standards und Richtlinien für das Testen von Komponenten des Furt API-Gateway-Projekts. Es soll sicherstellen, dass alle implementierten Funktionalitäten ausreichend durch Tests abgedeckt sind und die Tests konsistent und wartbar bleiben.
|
|
||||||
|
|
||||||
Es richtet sich an alle Entwickler, die Code zum Projekt beisteuern.
|
|
||||||
|
|
||||||
## 1. Grundprinzipien
|
|
||||||
|
|
||||||
### 1.1 Test-First-Entwicklung
|
|
||||||
|
|
||||||
- Tests sollten parallel zur Implementierung oder idealerweise vor der eigentlichen Implementierung geschrieben werden.
|
|
||||||
- Keine Implementierung gilt als abgeschlossen, bis entsprechende Tests vorhanden sind.
|
|
||||||
- Pull Requests ohne Tests werden in der Regel nicht akzeptiert.
|
|
||||||
|
|
||||||
### 1.2 Testabdeckung
|
|
||||||
|
|
||||||
- Angestrebte Testabdeckung für Gateway-Kern: mindestens 85%
|
|
||||||
- Angestrebte Testabdeckung für Services: mindestens 80%
|
|
||||||
- Angestrebte Testabdeckung für Shared-Libraries: mindestens 90%
|
|
||||||
- Besonders kritische Komponenten (Authentifizierung, Routing, Service-Proxy) sollten eine Abdeckung nahe 100% haben.
|
|
||||||
|
|
||||||
### 1.3 Test-Typen
|
|
||||||
|
|
||||||
Folgende Test-Typen werden im Projekt verwendet:
|
|
||||||
|
|
||||||
1. **Unit Tests**: Testen einzelner Funktionen/Methoden in Isolation
|
|
||||||
2. **Integration Tests**: Testen des Zusammenspiels von Komponenten
|
|
||||||
3. **API Tests**: Testen der API-Endpunkte des Gateways und Services
|
|
||||||
4. **Service Integration Tests**: Testen der Gateway ↔ Service-Kommunikation
|
|
||||||
5. **End-to-End Tests**: Testen der gesamten Request-Pipeline (Client → Gateway → Service)
|
|
||||||
6. **Performance Tests**: Load-Testing für Gateway und Services
|
|
||||||
|
|
||||||
## 2. Test-Struktur und Dateiorganisation
|
|
||||||
|
|
||||||
### 2.1 Dateistruktur
|
|
||||||
|
|
||||||
- Test-Dateien werden neben den zu testenden Dateien platziert und erhalten den Suffix `_test.go`
|
|
||||||
- Beispiel: `gateway.go` → `gateway_test.go`
|
|
||||||
- Integration-Tests werden in `tests/integration/` platziert
|
|
||||||
- End-to-End-Tests werden in `tests/e2e/` platziert
|
|
||||||
|
|
||||||
### 2.2 Namenkonventionen
|
|
||||||
|
|
||||||
- Testfunktionen folgen dem Format `Test<Komponente><Funktionsname><Szenario>`
|
|
||||||
- Beispiel: `TestGatewayRoutingWithValidAPIKey`, `TestServiceProxyWhenServiceUnavailable`
|
|
||||||
- Benchmark-Tests: `Benchmark<Funktionsname>`
|
|
||||||
- Example-Tests: `Example<Funktionsname>`
|
|
||||||
|
|
||||||
### 2.3 Testpakete
|
|
||||||
|
|
||||||
- Tests sollten im selben Paket wie der zu testende Code sein (kein separates `_test`-Paket)
|
|
||||||
- Dies ermöglicht das Testen von Funktionen, die nicht exportiert werden
|
|
||||||
- Ausnahme: Integration-Tests können separate Pakete verwenden
|
|
||||||
|
|
||||||
## 3. Unit Tests
|
|
||||||
|
|
||||||
### 3.1 Grundstruktur
|
|
||||||
|
|
||||||
Jeder Unit Test sollte folgende Struktur haben:
|
|
||||||
|
|
||||||
```go
|
|
||||||
func TestFunctionName(t *testing.T) {
|
|
||||||
// Arrange: Vorbereitung der Testdaten und Abhängigkeiten
|
|
||||||
input := setupTestInput()
|
|
||||||
mockService := &MockService{}
|
|
||||||
expected := expectedResult{}
|
|
||||||
|
|
||||||
// Act: Ausführen der zu testenden Funktion
|
|
||||||
actual, err := FunctionName(input, mockService)
|
|
||||||
|
|
||||||
// Assert: Überprüfung des Ergebnisses
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Expected no error, but got: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !reflect.DeepEqual(actual, expected) {
|
|
||||||
t.Errorf("Expected %+v, but got %+v", expected, actual)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3.2 Table-Driven Tests
|
|
||||||
|
|
||||||
Für komplexere Funktionen sollten Table-Driven Tests verwendet werden:
|
|
||||||
|
|
||||||
```go
|
|
||||||
func TestGatewayRouting(t *testing.T) {
|
|
||||||
testCases := []struct {
|
|
||||||
name string
|
|
||||||
requestPath string
|
|
||||||
expectedService string
|
|
||||||
expectedPath string
|
|
||||||
wantErr bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "formular2mail service routing",
|
|
||||||
requestPath: "/v1/mail/send",
|
|
||||||
expectedService: "formular2mail",
|
|
||||||
expectedPath: "/send",
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "sagjan service routing",
|
|
||||||
requestPath: "/v1/comments/list",
|
|
||||||
expectedService: "sagjan",
|
|
||||||
expectedPath: "/list",
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "unknown service",
|
|
||||||
requestPath: "/v1/unknown/test",
|
|
||||||
expectedService: "",
|
|
||||||
expectedPath: "",
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range testCases {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
gateway := setupTestGateway()
|
|
||||||
service, path, err := gateway.ResolveRoute(tc.requestPath)
|
|
||||||
|
|
||||||
if tc.wantErr && err == nil {
|
|
||||||
t.Error("Expected error, but got nil")
|
|
||||||
}
|
|
||||||
if !tc.wantErr && err != nil {
|
|
||||||
t.Fatalf("Expected no error, but got: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if service != tc.expectedService {
|
|
||||||
t.Errorf("Expected service %s, got %s", tc.expectedService, service)
|
|
||||||
}
|
|
||||||
if path != tc.expectedPath {
|
|
||||||
t.Errorf("Expected path %s, got %s", tc.expectedPath, path)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3.3 Mocking und Test-Doubles
|
|
||||||
|
|
||||||
- Für HTTP-Clients verwende `httptest.NewServer`
|
|
||||||
- Für Services erstelle Interface-basierte Mocks
|
|
||||||
- Verwende Dependency Injection für bessere Testbarkeit
|
|
||||||
|
|
||||||
```go
|
|
||||||
// Service Interface für Testbarkeit
|
|
||||||
type MailService interface {
|
|
||||||
SendMail(request MailRequest) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mock Implementation
|
|
||||||
type MockMailService struct {
|
|
||||||
SendMailFunc func(MailRequest) error
|
|
||||||
CallCount int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockMailService) SendMail(request MailRequest) error {
|
|
||||||
m.CallCount++
|
|
||||||
if m.SendMailFunc != nil {
|
|
||||||
return m.SendMailFunc(request)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test mit Mock
|
|
||||||
func TestFormular2MailHandler(t *testing.T) {
|
|
||||||
mockService := &MockMailService{
|
|
||||||
SendMailFunc: func(req MailRequest) error {
|
|
||||||
if req.Email == "invalid" {
|
|
||||||
return errors.New("invalid email")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
handler := NewFormular2MailHandler(mockService)
|
|
||||||
|
|
||||||
// Test ausführen...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 4. Integration Tests
|
|
||||||
|
|
||||||
### 4.1 Gateway-Service Integration Tests
|
|
||||||
|
|
||||||
Diese Tests prüfen die Kommunikation zwischen Gateway und Services:
|
|
||||||
|
|
||||||
```go
|
|
||||||
// tests/integration/gateway_service_test.go
|
|
||||||
func TestGatewayServiceIntegration(t *testing.T) {
|
|
||||||
// Setup Test-Services
|
|
||||||
mailService := startTestMailService(t)
|
|
||||||
defer mailService.Close()
|
|
||||||
|
|
||||||
commentsService := startTestCommentsService(t)
|
|
||||||
defer commentsService.Close()
|
|
||||||
|
|
||||||
// Setup Gateway mit Test-Konfiguration
|
|
||||||
gateway := setupTestGateway(t, GatewayConfig{
|
|
||||||
Services: map[string]ServiceConfig{
|
|
||||||
"formular2mail": {
|
|
||||||
Enabled: true,
|
|
||||||
PathPrefix: "/v1/mail",
|
|
||||||
Upstream: mailService.URL,
|
|
||||||
},
|
|
||||||
"sagjan": {
|
|
||||||
Enabled: true,
|
|
||||||
PathPrefix: "/v1/comments",
|
|
||||||
Upstream: commentsService.URL,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
defer gateway.Close()
|
|
||||||
|
|
||||||
// Test Gateway → Service Routing
|
|
||||||
t.Run("mail service integration", func(t *testing.T) {
|
|
||||||
resp := makeTestRequest(t, gateway.URL+"/v1/mail/send", "POST", mailRequestBody)
|
|
||||||
assertStatusCode(t, resp, http.StatusOK)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("comments service integration", func(t *testing.T) {
|
|
||||||
resp := makeTestRequest(t, gateway.URL+"/v1/comments", "GET", nil)
|
|
||||||
assertStatusCode(t, resp, http.StatusOK)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4.2 Database Integration Tests (für Services)
|
|
||||||
|
|
||||||
Für Services mit Datenbank-Zugriff:
|
|
||||||
|
|
||||||
```go
|
|
||||||
func TestSagjanServiceDatabaseIntegration(t *testing.T) {
|
|
||||||
// Setup Test-Database (SQLite in-memory)
|
|
||||||
db := setupTestDB(t)
|
|
||||||
defer db.Close()
|
|
||||||
|
|
||||||
service := NewSagjanService(db)
|
|
||||||
|
|
||||||
// Test Comment Creation
|
|
||||||
comment := &Comment{
|
|
||||||
PageURL: "https://example.com/test",
|
|
||||||
Author: "Test User",
|
|
||||||
Content: "Test Comment",
|
|
||||||
}
|
|
||||||
|
|
||||||
err := service.CreateComment(context.Background(), comment)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create comment: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test Comment Retrieval
|
|
||||||
comments, err := service.GetComments(context.Background(), "https://example.com/test")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to get comments: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(comments) != 1 {
|
|
||||||
t.Errorf("Expected 1 comment, got %d", len(comments))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func setupTestDB(t *testing.T) *sql.DB {
|
|
||||||
db, err := sql.Open("sqlite3", ":memory:")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to open test database: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run migrations
|
|
||||||
if err := runMigrations(db); err != nil {
|
|
||||||
t.Fatalf("Failed to run migrations: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return db
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 5. API Tests
|
|
||||||
|
|
||||||
### 5.1 Gateway API Tests
|
|
||||||
|
|
||||||
Tests für die Gateway-API-Endpunkte:
|
|
||||||
|
|
||||||
```go
|
|
||||||
func TestGatewayAPIEndpoints(t *testing.T) {
|
|
||||||
gateway := setupTestGateway(t)
|
|
||||||
defer gateway.Close()
|
|
||||||
|
|
||||||
testCases := []struct {
|
|
||||||
name string
|
|
||||||
method string
|
|
||||||
path string
|
|
||||||
headers map[string]string
|
|
||||||
body string
|
|
||||||
expectedStatus int
|
|
||||||
expectedBody string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "health check",
|
|
||||||
method: "GET",
|
|
||||||
path: "/health",
|
|
||||||
expectedStatus: http.StatusOK,
|
|
||||||
expectedBody: `{"status":"healthy"}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "unauthorized request",
|
|
||||||
method: "POST",
|
|
||||||
path: "/v1/mail/send",
|
|
||||||
expectedStatus: http.StatusUnauthorized,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "authorized mail request",
|
|
||||||
method: "POST",
|
|
||||||
path: "/v1/mail/send",
|
|
||||||
headers: map[string]string{
|
|
||||||
"X-API-Key": "test-api-key",
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: `{"name":"Test","email":"test@example.com","message":"Test"}`,
|
|
||||||
expectedStatus: http.StatusOK,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range testCases {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
req := createTestRequest(t, tc.method, gateway.URL+tc.path, tc.body)
|
|
||||||
|
|
||||||
for key, value := range tc.headers {
|
|
||||||
req.Header.Set(key, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := http.DefaultClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Request failed: %v", err)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode != tc.expectedStatus {
|
|
||||||
t.Errorf("Expected status %d, got %d", tc.expectedStatus, resp.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
if tc.expectedBody != "" {
|
|
||||||
body, _ := io.ReadAll(resp.Body)
|
|
||||||
if string(body) != tc.expectedBody {
|
|
||||||
t.Errorf("Expected body %s, got %s", tc.expectedBody, string(body))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5.2 Service API Tests
|
|
||||||
|
|
||||||
Tests für individuelle Service-APIs:
|
|
||||||
|
|
||||||
```go
|
|
||||||
func TestFormular2MailAPI(t *testing.T) {
|
|
||||||
service := startTestFormular2MailService(t)
|
|
||||||
defer service.Close()
|
|
||||||
|
|
||||||
t.Run("valid mail request", func(t *testing.T) {
|
|
||||||
reqBody := `{
|
|
||||||
"name": "John Doe",
|
|
||||||
"email": "john@example.com",
|
|
||||||
"message": "Test message"
|
|
||||||
}`
|
|
||||||
|
|
||||||
resp := makeTestRequest(t, service.URL+"/send", "POST", reqBody)
|
|
||||||
assertStatusCode(t, resp, http.StatusOK)
|
|
||||||
|
|
||||||
var response MailResponse
|
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
|
|
||||||
t.Fatalf("Failed to decode response: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !response.Success {
|
|
||||||
t.Error("Expected success=true in response")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("invalid mail request", func(t *testing.T) {
|
|
||||||
reqBody := `{"name": "", "email": "invalid", "message": ""}`
|
|
||||||
|
|
||||||
resp := makeTestRequest(t, service.URL+"/send", "POST", reqBody)
|
|
||||||
assertStatusCode(t, resp, http.StatusBadRequest)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 6. Performance Tests
|
|
||||||
|
|
||||||
### 6.1 Gateway Performance Tests
|
|
||||||
|
|
||||||
```go
|
|
||||||
func TestGatewayPerformance(t *testing.T) {
|
|
||||||
if testing.Short() {
|
|
||||||
t.Skip("Skipping performance test in short mode")
|
|
||||||
}
|
|
||||||
|
|
||||||
gateway := setupTestGateway(t)
|
|
||||||
defer gateway.Close()
|
|
||||||
|
|
||||||
// Load test
|
|
||||||
concurrency := 10
|
|
||||||
requests := 1000
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
errors := make(chan error, requests)
|
|
||||||
|
|
||||||
start := time.Now()
|
|
||||||
|
|
||||||
for i := 0; i < concurrency; i++ {
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
for j := 0; j < requests/concurrency; j++ {
|
|
||||||
resp, err := http.Get(gateway.URL + "/health")
|
|
||||||
if err != nil {
|
|
||||||
errors <- err
|
|
||||||
return
|
|
||||||
}
|
|
||||||
resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
errors <- fmt.Errorf("unexpected status: %d", resp.StatusCode)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
close(errors)
|
|
||||||
|
|
||||||
duration := time.Since(start)
|
|
||||||
|
|
||||||
// Check for errors
|
|
||||||
for err := range errors {
|
|
||||||
t.Errorf("Request error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Performance assertions
|
|
||||||
requestsPerSecond := float64(requests) / duration.Seconds()
|
|
||||||
if requestsPerSecond < 500 { // Minimum 500 RPS
|
|
||||||
t.Errorf("Performance too low: %.2f RPS", requestsPerSecond)
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Logf("Performance: %.2f RPS over %v", requestsPerSecond, duration)
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkGatewayRouting(b *testing.B) {
|
|
||||||
gateway := setupBenchmarkGateway(b)
|
|
||||||
|
|
||||||
req := httptest.NewRequest("GET", "/v1/mail/send", nil)
|
|
||||||
req.Header.Set("X-API-Key", "test-key")
|
|
||||||
|
|
||||||
b.ResetTimer()
|
|
||||||
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
gateway.ServeHTTP(w, req)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 7. Test-Daten und Test-Utilities
|
|
||||||
|
|
||||||
### 7.1 Test-Daten-Management
|
|
||||||
|
|
||||||
```go
|
|
||||||
// internal/testutil/fixtures.go
|
|
||||||
package testutil
|
|
||||||
|
|
||||||
func CreateTestMailRequest() MailRequest {
|
|
||||||
return MailRequest{
|
|
||||||
Name: "Test User",
|
|
||||||
Email: "test@example.com",
|
|
||||||
Subject: "Test Subject",
|
|
||||||
Message: "Test Message",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func CreateTestComment() *Comment {
|
|
||||||
return &Comment{
|
|
||||||
ID: uuid.New().String(),
|
|
||||||
PageURL: "https://example.com/test",
|
|
||||||
Author: "Test Author",
|
|
||||||
Email: "test@example.com",
|
|
||||||
Content: "Test Comment Content",
|
|
||||||
Status: StatusPending,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func CreateTestGatewayConfig() GatewayConfig {
|
|
||||||
return GatewayConfig{
|
|
||||||
Gateway: GatewaySettings{
|
|
||||||
Port: "8080",
|
|
||||||
LogLevel: "info",
|
|
||||||
},
|
|
||||||
Security: SecurityConfig{
|
|
||||||
APIKeys: []APIKey{
|
|
||||||
{
|
|
||||||
Key: "test-api-key",
|
|
||||||
Name: "Test Key",
|
|
||||||
Permissions: []string{"mail:send"},
|
|
||||||
AllowedIPs: []string{"127.0.0.1"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Services: map[string]ServiceConfig{
|
|
||||||
"formular2mail": {
|
|
||||||
Enabled: true,
|
|
||||||
PathPrefix: "/v1/mail",
|
|
||||||
Upstream: "http://127.0.0.1:8081",
|
|
||||||
HealthCheck: "/health",
|
|
||||||
Timeout: 30 * time.Second,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 7.2 Test-Helper-Funktionen
|
|
||||||
|
|
||||||
```go
|
|
||||||
// internal/testutil/helpers.go
|
|
||||||
package testutil
|
|
||||||
|
|
||||||
func AssertStatusCode(t *testing.T, resp *http.Response, expected int) {
|
|
||||||
t.Helper()
|
|
||||||
if resp.StatusCode != expected {
|
|
||||||
t.Errorf("Expected status code %d, got %d", expected, resp.StatusCode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func AssertResponseBody(t *testing.T, resp *http.Response, expected string) {
|
|
||||||
t.Helper()
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to read response body: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if string(body) != expected {
|
|
||||||
t.Errorf("Expected body %q, got %q", expected, string(body))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func MakeTestRequest(t *testing.T, url, method, body string) *http.Response {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
var reqBody io.Reader
|
|
||||||
if body != "" {
|
|
||||||
reqBody = strings.NewReader(body)
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err := http.NewRequest(method, url, reqBody)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create request: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if body != "" {
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := http.DefaultClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Request failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return resp
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 8. Test-Umgebung und CI
|
|
||||||
|
|
||||||
### 8.1 Lokale Tests
|
|
||||||
|
|
||||||
- Alle Tests sollten mit `go test ./...` ausführbar sein
|
|
||||||
- Keine Tests sollten externe Ressourcen benötigen (wie echte E-Mail-Server)
|
|
||||||
- Performance-Tests mit `-short` Flag überspringen
|
|
||||||
|
|
||||||
### 8.2 Test-Tags
|
|
||||||
|
|
||||||
```go
|
|
||||||
// +build integration
|
|
||||||
|
|
||||||
package tests
|
|
||||||
|
|
||||||
// Integration tests that require external resources
|
|
||||||
```
|
|
||||||
|
|
||||||
**Ausführung:**
|
|
||||||
```bash
|
|
||||||
# Nur Unit Tests
|
|
||||||
go test ./...
|
|
||||||
|
|
||||||
# Mit Integration Tests
|
|
||||||
go test -tags=integration ./...
|
|
||||||
|
|
||||||
# Mit Performance Tests
|
|
||||||
go test -timeout=30m ./...
|
|
||||||
|
|
||||||
# Kurze Tests für CI
|
|
||||||
go test -short ./...
|
|
||||||
```
|
|
||||||
|
|
||||||
### 8.3 Coverage-Berichte
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Coverage generieren
|
|
||||||
go test -coverprofile=coverage.out ./...
|
|
||||||
|
|
||||||
# HTML-Report
|
|
||||||
go tool cover -html=coverage.out -o coverage.html
|
|
||||||
|
|
||||||
# Coverage-Threshold prüfen
|
|
||||||
go tool cover -func=coverage.out | grep total | awk '{print $3}' | sed 's/%//'
|
|
||||||
```
|
|
||||||
|
|
||||||
## 9. Spezifische Testfälle für Furt
|
|
||||||
|
|
||||||
### 9.1 Gateway-Routing Tests
|
|
||||||
|
|
||||||
```go
|
|
||||||
func TestGatewayServiceRouting(t *testing.T) {
|
|
||||||
testCases := []struct {
|
|
||||||
name string
|
|
||||||
requestPath string
|
|
||||||
method string
|
|
||||||
expectedService string
|
|
||||||
expectedUpstream string
|
|
||||||
wantErr bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "formular2mail routing",
|
|
||||||
requestPath: "/v1/mail/send",
|
|
||||||
method: "POST",
|
|
||||||
expectedService: "formular2mail",
|
|
||||||
expectedUpstream: "http://127.0.0.1:8081",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "sagjan comments routing",
|
|
||||||
requestPath: "/v1/comments",
|
|
||||||
method: "GET",
|
|
||||||
expectedService: "sagjan",
|
|
||||||
expectedUpstream: "http://127.0.0.1:8082",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "unknown service",
|
|
||||||
requestPath: "/v1/unknown",
|
|
||||||
method: "GET",
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Implementation...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 9.2 Authentication Tests
|
|
||||||
|
|
||||||
```go
|
|
||||||
func TestGatewayAuthentication(t *testing.T) {
|
|
||||||
testCases := []struct {
|
|
||||||
name string
|
|
||||||
apiKey string
|
|
||||||
clientIP string
|
|
||||||
requestPath string
|
|
||||||
expectedStatus int
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "valid API key and IP",
|
|
||||||
apiKey: "hugo-frontend-key",
|
|
||||||
clientIP: "127.0.0.1",
|
|
||||||
requestPath: "/v1/mail/send",
|
|
||||||
expectedStatus: http.StatusOK,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid API key",
|
|
||||||
apiKey: "invalid-key",
|
|
||||||
clientIP: "127.0.0.1",
|
|
||||||
requestPath: "/v1/mail/send",
|
|
||||||
expectedStatus: http.StatusUnauthorized,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "blocked IP",
|
|
||||||
apiKey: "hugo-frontend-key",
|
|
||||||
clientIP: "192.168.1.100",
|
|
||||||
requestPath: "/v1/mail/send",
|
|
||||||
expectedStatus: http.StatusForbidden,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Implementation...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 9.3 Service Health Check Tests
|
|
||||||
|
|
||||||
```go
|
|
||||||
func TestServiceHealthChecks(t *testing.T) {
|
|
||||||
// Test Gateway health aggregation
|
|
||||||
t.Run("all services healthy", func(t *testing.T) {
|
|
||||||
// Setup healthy services
|
|
||||||
// Test /health returns 200 with all services status
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("one service unhealthy", func(t *testing.T) {
|
|
||||||
// Setup one failing service
|
|
||||||
// Test /health returns appropriate status
|
|
||||||
})
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 10. Test-Automation und CI-Integration
|
|
||||||
|
|
||||||
### 10.1 GitHub Actions / Gitea Actions
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# .gitea/workflows/test.yml
|
|
||||||
name: Test Suite
|
|
||||||
|
|
||||||
on: [push, pull_request]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Set up Go
|
|
||||||
uses: actions/setup-go@v3
|
|
||||||
with:
|
|
||||||
go-version: 1.21
|
|
||||||
|
|
||||||
- name: Run Unit Tests
|
|
||||||
run: go test -short -race -coverprofile=coverage.out ./...
|
|
||||||
|
|
||||||
- name: Run Integration Tests
|
|
||||||
run: go test -tags=integration ./tests/integration/
|
|
||||||
|
|
||||||
- name: Check Coverage
|
|
||||||
run: |
|
|
||||||
coverage=$(go tool cover -func=coverage.out | grep total | awk '{print $3}' | sed 's/%//')
|
|
||||||
if (( $(echo "$coverage < 80" | bc -l) )); then
|
|
||||||
echo "Coverage $coverage% is below 80%"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
```
|
|
||||||
|
|
||||||
## 11. Best Practices Zusammenfassung
|
|
||||||
|
|
||||||
### 11.1 Do's
|
|
||||||
|
|
||||||
- ✅ **Testbare Architektur:** Dependency Injection verwenden
|
|
||||||
- ✅ **Isolierte Tests:** Keine Abhängigkeiten zwischen Tests
|
|
||||||
- ✅ **Realistische Test-Daten:** Aber anonymisiert und minimal
|
|
||||||
- ✅ **Performance-bewusst:** Benchmarks für kritische Pfade
|
|
||||||
- ✅ **Dokumentierte Test-Fälle:** Klare Beschreibungen der Test-Szenarien
|
|
||||||
|
|
||||||
### 11.2 Don'ts
|
|
||||||
|
|
||||||
- ❌ **Externe Ressourcen:** Keine echten E-Mail-Server, externe APIs
|
|
||||||
- ❌ **Feste Zeitstempel:** `time.Now()` mocken in Tests
|
|
||||||
- ❌ **Globaler State:** Tests sollten unabhängig sein
|
|
||||||
- ❌ **Überflüssige Tests:** Triviale Getter/Setter nicht testen
|
|
||||||
- ❌ **Fragile Tests:** Tests sollen bei kleinen Änderungen nicht brechen
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Diese Richtlinien sollen als Leitfaden dienen und können im Laufe des Projekts angepasst und erweitert werden. Bei Unklarheiten oder Fragen zu diesen Richtlinien kann das Entwicklungsteam kontaktiert werden.
|
|
||||||
3
go.mod
3
go.mod
|
|
@ -1,3 +0,0 @@
|
||||||
module furt
|
|
||||||
|
|
||||||
go 1.24.3
|
|
||||||
38
projct-tree.txt
Normal file
38
projct-tree.txt
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
.
|
||||||
|
├── furt-lua
|
||||||
|
│ ├── config
|
||||||
|
│ │ └── server.lua
|
||||||
|
│ ├── deployment
|
||||||
|
│ │ └── openbsd
|
||||||
|
│ │ └── rc.d-furt
|
||||||
|
│ ├── production_checklist.md
|
||||||
|
│ ├── README.md
|
||||||
|
│ ├── scripts
|
||||||
|
│ │ ├── cleanup_debug.sh
|
||||||
|
│ │ ├── manual_mail_test.sh
|
||||||
|
│ │ ├── production_test_sequence.sh
|
||||||
|
│ │ ├── setup_env.sh
|
||||||
|
│ │ ├── start.sh
|
||||||
|
│ │ ├── stress_test.sh
|
||||||
|
│ │ ├── test_auth.sh
|
||||||
|
│ │ ├── test_curl.sh
|
||||||
|
│ │ ├── test_modular.sh
|
||||||
|
│ │ └── test_smtp.sh
|
||||||
|
│ ├── src
|
||||||
|
│ │ ├── auth.lua
|
||||||
|
│ │ ├── ip_utils.lua
|
||||||
|
│ │ ├── main.lua
|
||||||
|
│ │ ├── rate_limiter.lua
|
||||||
|
│ │ ├── routes
|
||||||
|
│ │ │ ├── auth.lua
|
||||||
|
│ │ │ └── mail.lua
|
||||||
|
│ │ └── smtp.lua
|
||||||
|
│ └── tests
|
||||||
|
│ └── test_http.lua
|
||||||
|
├── LICENSE
|
||||||
|
├── projct-tree.txt
|
||||||
|
├── README.md
|
||||||
|
└── tools
|
||||||
|
└── gitea -> /home/michael/tools/tool-gitea-workflow/scripts
|
||||||
|
|
||||||
|
11 directories, 25 files
|
||||||
|
|
@ -1,779 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
# scripts/create_issue.sh - Furt API Gateway Issue Creator
|
|
||||||
# DEBUG VERSION with path fixes and diagnostic output
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# Standard environment setup
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
|
|
||||||
|
|
||||||
if [ -f "$PROJECT_ROOT/.env" ]; then
|
|
||||||
export $(cat "$PROJECT_ROOT/.env" | grep -v '^#' | xargs)
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Colors
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
BLUE='\033[0;34m'
|
|
||||||
RED='\033[0;31m'
|
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
CYAN='\033[0;36m'
|
|
||||||
NC='\033[0m'
|
|
||||||
|
|
||||||
log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
|
|
||||||
log_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
|
|
||||||
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
|
||||||
log_warning() { echo -e "${YELLOW}[WARNING]${NC} $1"; }
|
|
||||||
log_debug() {
|
|
||||||
if [[ "${DEBUG:-}" == "1" ]]; then
|
|
||||||
echo -e "${CYAN}[DEBUG]${NC} $1" >&2;
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Track new labels for auto-update (FIXED: Safe initialization)
|
|
||||||
declare -A NEW_LABELS_CREATED=()
|
|
||||||
|
|
||||||
# === LABEL DEFINITIONS START ===
|
|
||||||
# This section is auto-maintained by update_script_labels.sh
|
|
||||||
# DO NOT EDIT MANUALLY - Changes will be overwritten
|
|
||||||
declare -A LABEL_DEFINITIONS=(
|
|
||||||
["hugo-integration"]="color:ff7518;context:frontend;usage:hugo_templates,integration"
|
|
||||||
["service-newsletter"]="color:ff6b6b;context:newsletter;usage:newsletter_integration"
|
|
||||||
["service-analytics"]="color:1d76db;context:service_integration;usage:service_specific"
|
|
||||||
["ready-for-deployment"]="color:28a745;context:deploy_ready;usage:status_updates"
|
|
||||||
["service-clean-test4"]="color:1d76db;context:service_integration;usage:service_specific"
|
|
||||||
["service-completely-absolut-new7"]="color:1d76db;context:service_integration;usage:service_specific"
|
|
||||||
["service-completely-absolut-new9"]="color:1d76db;context:service_integration;usage:service_specific"
|
|
||||||
["service-completely-absolut-new8"]="color:1d76db;context:service_integration;usage:service_specific"
|
|
||||||
["performance"]="color:fbca04;context:optimization;usage:performance_template,architecture_template"
|
|
||||||
["bug"]="color:d73a4a;context:error;usage:bug_template,status_updates"
|
|
||||||
["question"]="color:d876e3;context:discussion;usage:question_template"
|
|
||||||
["service-formular2mail"]="color:1d76db;context:formular2mail;usage:formular2mail_integration"
|
|
||||||
["good-first-issue"]="color:7057ff;context:beginner_friendly;usage:manual_assignment"
|
|
||||||
["service-completely-absolut-new10"]="color:1d76db;context:service_integration;usage:service_specific"
|
|
||||||
["service-completely-absolut-new11"]="color:1d76db;context:service_integration;usage:service_specific"
|
|
||||||
["breaking-change"]="color:d73a4a;context:breaking;usage:api_templates,architecture_template"
|
|
||||||
["service-request"]="color:7057ff;context:new_service;usage:service_templates,status_updates"
|
|
||||||
["service-debug-test"]="color:1d76db;context:service_integration;usage:service_specific"
|
|
||||||
["low-priority"]="color:0e8a16;context:nice_to_have;usage:all_templates"
|
|
||||||
["blocked"]="color:d73a4a;context:blocked;usage:status_updates"
|
|
||||||
["low-tech"]="color:6f42c1;context:low_tech_principle;usage:architecture_template,performance_template,security_template"
|
|
||||||
["deployment"]="color:ff7518;context:deployment;usage:deployment_template"
|
|
||||||
["gateway"]="color:0052cc;context:gateway_core;usage:architecture_template,performance_template,service_templates"
|
|
||||||
["service-sagjan"]="color:1d76db;context:sagjan;usage:sagjan_integration"
|
|
||||||
["work-in-progress"]="color:fbca04;context:active;usage:status_updates"
|
|
||||||
["service-debug-check-final2"]="color:1d76db;context:service_integration;usage:service_specific"
|
|
||||||
["digital-sovereignty"]="color:6f42c1;context:digital_sovereignty;usage:architecture_template,performance_template,security_template"
|
|
||||||
["security"]="color:28a745;context:security_review;usage:security_template,architecture_template"
|
|
||||||
["architecture"]="color:d4c5f9;context:design;usage:architecture_template,gateway"
|
|
||||||
["configuration"]="color:f9d71c;context:config_management;usage:deployment_template,architecture_template"
|
|
||||||
["needs-review"]="color:0e8a16;context:review;usage:status_updates"
|
|
||||||
["help-wanted"]="color:159818;context:community_help;usage:manual_assignment"
|
|
||||||
["service-whatever-you-want"]="color:1d76db;context:service_integration;usage:service_specific"
|
|
||||||
["api-contract"]="color:5319e7;context:api_design;usage:api_templates,service_templates"
|
|
||||||
["enhancement"]="color:84b6eb;context:improvement;usage:all_templates"
|
|
||||||
["high-priority"]="color:d73a4a;context:urgent;usage:all_templates"
|
|
||||||
["testing"]="color:f9d71c;context:testing;usage:testing_template,integration"
|
|
||||||
["test-all-templates"]="color:ff0000;context:test;usage:all_templates"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Extract label info
|
|
||||||
get_label_color() { echo "${LABEL_DEFINITIONS[$1]}" | cut -d';' -f1 | cut -d':' -f2; }
|
|
||||||
get_label_context() { echo "${LABEL_DEFINITIONS[$1]}" | cut -d';' -f2 | cut -d':' -f2; }
|
|
||||||
get_label_usage() { echo "${LABEL_DEFINITIONS[$1]}" | cut -d';' -f3 | cut -d':' -f2; }
|
|
||||||
|
|
||||||
# Check if label is valid for context
|
|
||||||
is_label_valid_for_context() {
|
|
||||||
local label="$1"
|
|
||||||
local context="$2"
|
|
||||||
local usage=$(get_label_usage "$label")
|
|
||||||
[[ "$usage" == *"$context"* ]] || [[ "$usage" == "all_templates" ]]
|
|
||||||
}
|
|
||||||
# === LABEL DEFINITIONS END ===
|
|
||||||
|
|
||||||
# === TEMPLATE LABEL MAPPINGS START ===
|
|
||||||
# Auto-generated template to label mappings
|
|
||||||
declare -A TEMPLATE_LABELS=(
|
|
||||||
["performance"]="performance,low-priority,low-tech,gateway,digital-sovereignty,enhancement,high-priority,test-all-templates"
|
|
||||||
["bug"]="bug,low-priority,enhancement,high-priority,test-all-templates"
|
|
||||||
["api"]="breaking-change,low-priority,api-contract,enhancement,high-priority,test-all-templates"
|
|
||||||
["service"]="low-priority,gateway,api-contract,enhancement,high-priority,test-all-templates"
|
|
||||||
["deployment"]="low-priority,deployment,configuration,enhancement,high-priority,test-all-templates"
|
|
||||||
["security"]="low-priority,low-tech,digital-sovereignty,security,enhancement,high-priority,test-all-templates"
|
|
||||||
["architecture"]="performance,breaking-change,low-priority,low-tech,gateway,digital-sovereignty,security,architecture,configuration,enhancement,high-priority,test-all-templates"
|
|
||||||
["hugo"]="hugo-integration,low-priority,enhancement,high-priority,test-all-templates"
|
|
||||||
)
|
|
||||||
# === TEMPLATE LABEL MAPPINGS END ===
|
|
||||||
|
|
||||||
# Load existing labels from repository
|
|
||||||
declare -A LABEL_IDS
|
|
||||||
|
|
||||||
load_existing_labels() {
|
|
||||||
if [[ -z "${GITEA_URL:-}" ]] || [[ -z "${GITEA_TOKEN:-}" ]]; then
|
|
||||||
log_error "GITEA_URL and GITEA_TOKEN must be set"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
log_info "Loading existing labels from repository..."
|
|
||||||
|
|
||||||
local response=$(curl -s "$GITEA_URL/api/v1/repos/$REPO_OWNER/$REPO_NAME/labels" \
|
|
||||||
-H "Authorization: token $GITEA_TOKEN")
|
|
||||||
|
|
||||||
if [[ $? -ne 0 ]]; then
|
|
||||||
log_error "Failed to fetch labels from repository"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
while IFS= read -r line; do
|
|
||||||
local name=$(echo "$line" | jq -r '.name')
|
|
||||||
local id=$(echo "$line" | jq -r '.id')
|
|
||||||
LABEL_IDS["$name"]="$id"
|
|
||||||
done < <(echo "$response" | jq -c '.[]')
|
|
||||||
|
|
||||||
log_info "Loaded ${#LABEL_IDS[@]} existing labels"
|
|
||||||
}
|
|
||||||
|
|
||||||
# FIXED: Silent version of ensure_label_exists (no stdout pollution!)
|
|
||||||
ensure_label_exists_silent() {
|
|
||||||
local name="$1"
|
|
||||||
local color="${2:-ff6b6b}"
|
|
||||||
local description="${3:-Auto-generated label}"
|
|
||||||
local usage="${4:-manual_assignment}" # ADDED: usage parameter
|
|
||||||
|
|
||||||
log_debug "Checking label: $name"
|
|
||||||
|
|
||||||
if [[ -n "${LABEL_IDS[$name]:-}" ]]; then
|
|
||||||
log_debug "Label $name already exists (ID: ${LABEL_IDS[$name]})"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
log_debug "Creating new label: $name with color $color"
|
|
||||||
|
|
||||||
# Create label (redirect output to prevent stdout mixing)
|
|
||||||
local response=$(curl -s -w "\n%{http_code}" -X POST \
|
|
||||||
"$GITEA_URL/api/v1/repos/$REPO_OWNER/$REPO_NAME/labels" \
|
|
||||||
-H "Authorization: token $GITEA_TOKEN" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d "{
|
|
||||||
\"name\": \"$name\",
|
|
||||||
\"color\": \"$color\",
|
|
||||||
\"description\": \"$description\"
|
|
||||||
}" 2>/dev/null)
|
|
||||||
|
|
||||||
local http_code=$(echo "$response" | tail -n1)
|
|
||||||
local response_body=$(echo "$response" | head -n -1)
|
|
||||||
|
|
||||||
if [[ "$http_code" == "201" ]]; then
|
|
||||||
local new_id=$(echo "$response_body" | jq -r '.id')
|
|
||||||
LABEL_IDS["$name"]="$new_id"
|
|
||||||
|
|
||||||
# FIXED: Track for auto-update with correct usage
|
|
||||||
NEW_LABELS_CREATED["$name"]="$color:auto_generated:$usage"
|
|
||||||
log_debug "Successfully created label $name (ID: $new_id)"
|
|
||||||
log_debug "Added to NEW_LABELS_CREATED: $name -> ${NEW_LABELS_CREATED[$name]}"
|
|
||||||
return 0
|
|
||||||
else
|
|
||||||
log_debug "Failed to create label $name (HTTP: $http_code)"
|
|
||||||
log_debug "Response: $response_body"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Process labels for template (updates global arrays, no output)
|
|
||||||
process_labels_for_template() {
|
|
||||||
local template="$1"
|
|
||||||
shift
|
|
||||||
local additional_labels=("$@")
|
|
||||||
|
|
||||||
log_debug "Processing labels for template: $template"
|
|
||||||
log_debug "Additional labels: ${additional_labels[*]}"
|
|
||||||
|
|
||||||
# Get template labels
|
|
||||||
local template_labels_string="${TEMPLATE_LABELS[$template]:-}"
|
|
||||||
local all_labels=()
|
|
||||||
|
|
||||||
# Add template labels
|
|
||||||
if [[ -n "$template_labels_string" ]]; then
|
|
||||||
IFS=',' read -ra template_labels <<< "$template_labels_string"
|
|
||||||
all_labels+=("${template_labels[@]}")
|
|
||||||
log_debug "Template labels: ${template_labels[*]}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Add additional labels
|
|
||||||
all_labels+=("${additional_labels[@]}")
|
|
||||||
log_debug "All labels to process: ${all_labels[*]}"
|
|
||||||
|
|
||||||
# Process all labels and ensure they exist
|
|
||||||
for label in "${all_labels[@]}"; do
|
|
||||||
log_debug "Processing label: $label"
|
|
||||||
|
|
||||||
# Process both known and unknown labels
|
|
||||||
if [[ -n "${LABEL_DEFINITIONS[$label]:-}" ]]; then
|
|
||||||
log_debug "Known label: $label"
|
|
||||||
# Known label - use defined color and context
|
|
||||||
local color=$(get_label_color "$label")
|
|
||||||
local context=$(get_label_context "$label")
|
|
||||||
|
|
||||||
ensure_label_exists_silent "$label" "$color" "Furt: $context"
|
|
||||||
else
|
|
||||||
log_debug "Unknown label: $label - creating with smart defaults"
|
|
||||||
# Unknown label - auto-create with smart defaults
|
|
||||||
local default_color="ff6b6b"
|
|
||||||
local default_context="auto_generated"
|
|
||||||
|
|
||||||
# Smart defaults based on label pattern
|
|
||||||
if [[ "$label" == service-* ]]; then
|
|
||||||
default_color="1d76db"
|
|
||||||
default_context="service_integration"
|
|
||||||
default_usage="service_specific" # FIXED: Not all_templates!
|
|
||||||
log_debug "Service label detected - using blue color and service_specific usage"
|
|
||||||
elif [[ "$label" == *-priority ]]; then
|
|
||||||
default_color="d73a4a"
|
|
||||||
default_context="priority_level"
|
|
||||||
default_usage="priority_management"
|
|
||||||
log_debug "Priority label detected - using red color"
|
|
||||||
elif [[ "$label" == hugo-* ]]; then
|
|
||||||
default_color="ff7518"
|
|
||||||
default_context="frontend_integration"
|
|
||||||
default_usage="hugo_integration"
|
|
||||||
log_debug "Hugo label detected - using orange color"
|
|
||||||
else
|
|
||||||
default_usage="manual_assignment"
|
|
||||||
fi
|
|
||||||
|
|
||||||
ensure_label_exists_silent "$label" "$default_color" "Furt: $default_context"
|
|
||||||
|
|
||||||
# FIXED: Track with correct usage
|
|
||||||
if [[ -n "${LABEL_IDS[$label]:-}" ]] && [[ -n "${NEW_LABELS_CREATED[$label]:-}" ]]; then
|
|
||||||
NEW_LABELS_CREATED["$label"]="$default_color:$default_context:$default_usage"
|
|
||||||
log_debug "Updated NEW_LABELS_CREATED with correct usage: $label -> $default_color:$default_context:$default_usage"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Debug: Check if this label was newly created
|
|
||||||
if [[ -n "${LABEL_IDS[$label]:-}" ]]; then
|
|
||||||
if [[ -n "${NEW_LABELS_CREATED[$label]:-}" ]]; then
|
|
||||||
log_debug " → Label $label was newly created and tracked"
|
|
||||||
else
|
|
||||||
log_debug " → Label $label already existed"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
log_warning "Failed to process label: $label"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
# Debug: Check NEW_LABELS_CREATED at end of processing
|
|
||||||
log_debug "NEW_LABELS_CREATED after processing: ${#NEW_LABELS_CREATED[@]} entries"
|
|
||||||
if [[ "${#NEW_LABELS_CREATED[@]}" -gt 0 ]] 2>/dev/null; then
|
|
||||||
for label_name in "${!NEW_LABELS_CREATED[@]}"; do
|
|
||||||
log_debug " - $label_name: ${NEW_LABELS_CREATED[$label_name]}"
|
|
||||||
done
|
|
||||||
else
|
|
||||||
log_debug " (no entries in NEW_LABELS_CREATED array)"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Build JSON from already processed labels (pure function, no side effects)
|
|
||||||
build_labels_json_from_processed() {
|
|
||||||
local template="$1"
|
|
||||||
shift
|
|
||||||
local additional_labels=("$@")
|
|
||||||
|
|
||||||
log_debug "Building JSON from processed labels"
|
|
||||||
|
|
||||||
# Get template labels
|
|
||||||
local template_labels_string="${TEMPLATE_LABELS[$template]:-}"
|
|
||||||
local all_labels=()
|
|
||||||
|
|
||||||
# Add template labels
|
|
||||||
if [[ -n "$template_labels_string" ]]; then
|
|
||||||
IFS=',' read -ra template_labels <<< "$template_labels_string"
|
|
||||||
all_labels+=("${template_labels[@]}")
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Add additional labels
|
|
||||||
all_labels+=("${additional_labels[@]}")
|
|
||||||
|
|
||||||
# Collect IDs from already processed labels
|
|
||||||
local label_ids=()
|
|
||||||
for label in "${all_labels[@]}"; do
|
|
||||||
if [[ -n "${LABEL_IDS[$label]:-}" ]]; then
|
|
||||||
label_ids+=("${LABEL_IDS[$label]}")
|
|
||||||
log_debug "Added ID ${LABEL_IDS[$label]} for $label to JSON"
|
|
||||||
else
|
|
||||||
log_warning "No ID found for label: $label"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
log_debug "Final label IDs for JSON: ${label_ids[*]}"
|
|
||||||
|
|
||||||
# Build JSON array (clean output only!)
|
|
||||||
if [[ ${#label_ids[@]} -gt 0 ]]; then
|
|
||||||
printf '[%s]' "$(IFS=','; echo "${label_ids[*]}")"
|
|
||||||
else
|
|
||||||
echo "[]"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# DEPRECATED: Old build_labels_json function (kept for compatibility)
|
|
||||||
build_labels_json() {
|
|
||||||
local template="$1"
|
|
||||||
shift
|
|
||||||
local additional_labels=("$@")
|
|
||||||
|
|
||||||
log_debug "Building labels for template: $template"
|
|
||||||
log_debug "Additional labels: ${additional_labels[*]}"
|
|
||||||
|
|
||||||
# Get template labels
|
|
||||||
local template_labels_string="${TEMPLATE_LABELS[$template]:-}"
|
|
||||||
local all_labels=()
|
|
||||||
|
|
||||||
# Add template labels
|
|
||||||
if [[ -n "$template_labels_string" ]]; then
|
|
||||||
IFS=',' read -ra template_labels <<< "$template_labels_string"
|
|
||||||
all_labels+=("${template_labels[@]}")
|
|
||||||
log_debug "Template labels: ${template_labels[*]}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Add additional labels
|
|
||||||
all_labels+=("${additional_labels[@]}")
|
|
||||||
log_debug "All labels to process: ${all_labels[*]}"
|
|
||||||
|
|
||||||
# FIXED: Ensure all labels exist and collect IDs (handles unknown labels!)
|
|
||||||
local label_ids=()
|
|
||||||
for label in "${all_labels[@]}"; do
|
|
||||||
log_debug "Processing label: $label"
|
|
||||||
|
|
||||||
# Process both known and unknown labels
|
|
||||||
if [[ -n "${LABEL_DEFINITIONS[$label]:-}" ]]; then
|
|
||||||
log_debug "Known label: $label"
|
|
||||||
# Known label - use defined color and context
|
|
||||||
local color=$(get_label_color "$label")
|
|
||||||
local context=$(get_label_context "$label")
|
|
||||||
|
|
||||||
ensure_label_exists_silent "$label" "$color" "Furt: $context"
|
|
||||||
else
|
|
||||||
log_debug "Unknown label: $label - creating with smart defaults"
|
|
||||||
# FIXED: Unknown label - auto-create with smart defaults
|
|
||||||
local default_color="ff6b6b"
|
|
||||||
local default_context="auto_generated"
|
|
||||||
|
|
||||||
# Smart defaults based on label pattern
|
|
||||||
if [[ "$label" == service-* ]]; then
|
|
||||||
default_color="1d76db"
|
|
||||||
default_context="service_integration"
|
|
||||||
log_debug "Service label detected - using blue color"
|
|
||||||
elif [[ "$label" == *-priority ]]; then
|
|
||||||
default_color="d73a4a"
|
|
||||||
default_context="priority_level"
|
|
||||||
log_debug "Priority label detected - using red color"
|
|
||||||
elif [[ "$label" == hugo-* ]]; then
|
|
||||||
default_color="ff7518"
|
|
||||||
default_context="frontend_integration"
|
|
||||||
log_debug "Hugo label detected - using orange color"
|
|
||||||
fi
|
|
||||||
|
|
||||||
ensure_label_exists_silent "$label" "$default_color" "Furt: $default_context"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Collect ID if label was created/exists
|
|
||||||
if [[ -n "${LABEL_IDS[$label]:-}" ]]; then
|
|
||||||
label_ids+=("${LABEL_IDS[$label]}")
|
|
||||||
log_debug "Added label ID: ${LABEL_IDS[$label]} for $label"
|
|
||||||
|
|
||||||
# Debug: Check if this label was newly created
|
|
||||||
if [[ -n "${NEW_LABELS_CREATED[$label]:-}" ]]; then
|
|
||||||
log_debug " → This label was newly created and tracked"
|
|
||||||
else
|
|
||||||
log_debug " → This label already existed"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
log_warning "Failed to get ID for label: $label"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
log_debug "Final label IDs: ${label_ids[*]}"
|
|
||||||
|
|
||||||
# Debug: Check NEW_LABELS_CREATED at end of function
|
|
||||||
log_debug "NEW_LABELS_CREATED at end of build_labels_json: ${#NEW_LABELS_CREATED[@]} entries"
|
|
||||||
if [[ "${#NEW_LABELS_CREATED[@]}" -gt 0 ]] 2>/dev/null; then
|
|
||||||
for label_name in "${!NEW_LABELS_CREATED[@]}"; do
|
|
||||||
log_debug " - $label_name: ${NEW_LABELS_CREATED[$label_name]}"
|
|
||||||
done
|
|
||||||
else
|
|
||||||
log_debug " (no entries in NEW_LABELS_CREATED array)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Build JSON array (clean output only!)
|
|
||||||
if [[ ${#label_ids[@]} -gt 0 ]]; then
|
|
||||||
printf '[%s]' "$(IFS=','; echo "${label_ids[*]}")"
|
|
||||||
else
|
|
||||||
echo "[]"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Show which labels are being used (AFTER JSON building to avoid stdout pollution)
|
|
||||||
show_labels_used() {
|
|
||||||
local template="$1"
|
|
||||||
shift
|
|
||||||
local additional_labels=("$@")
|
|
||||||
|
|
||||||
log_info "Labels used for this issue:"
|
|
||||||
|
|
||||||
# Show template labels
|
|
||||||
local template_labels_string="${TEMPLATE_LABELS[$template]:-}"
|
|
||||||
if [[ -n "$template_labels_string" ]]; then
|
|
||||||
IFS=',' read -ra template_labels <<< "$template_labels_string"
|
|
||||||
for label in "${template_labels[@]}"; do
|
|
||||||
if [[ -n "${LABEL_IDS[$label]:-}" ]]; then
|
|
||||||
log_info " ✅ $label (ID: ${LABEL_IDS[$label]})"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Show additional labels (these may have been newly created)
|
|
||||||
for label in "${additional_labels[@]}"; do
|
|
||||||
if [[ -n "${LABEL_IDS[$label]:-}" ]]; then
|
|
||||||
if [[ -n "${NEW_LABELS_CREATED[$label]:-}" ]]; then
|
|
||||||
log_info " ✅ $label (ID: ${LABEL_IDS[$label]}) [NEW!]"
|
|
||||||
else
|
|
||||||
log_info " ✅ $label (ID: ${LABEL_IDS[$label]})"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
log_warning " ❌ $label (failed to create)"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
# FIXED: AUTO-UPDATE with safe array handling and correct path
|
|
||||||
auto_update_scripts_if_needed() {
|
|
||||||
# FIXED: Safe check for empty associative array
|
|
||||||
local new_labels_count=0
|
|
||||||
if [[ "${#NEW_LABELS_CREATED[@]}" -gt 0 ]] 2>/dev/null; then
|
|
||||||
new_labels_count=${#NEW_LABELS_CREATED[@]}
|
|
||||||
fi
|
|
||||||
|
|
||||||
log_debug "Auto-update check: $new_labels_count new labels created"
|
|
||||||
|
|
||||||
# Debug: Show what's in NEW_LABELS_CREATED
|
|
||||||
if [[ $new_labels_count -gt 0 ]]; then
|
|
||||||
log_debug "NEW_LABELS_CREATED contents:"
|
|
||||||
for label_name in "${!NEW_LABELS_CREATED[@]}"; do
|
|
||||||
log_debug " - $label_name: ${NEW_LABELS_CREATED[$label_name]}"
|
|
||||||
done
|
|
||||||
else
|
|
||||||
log_debug "NEW_LABELS_CREATED is empty or unset"
|
|
||||||
# Debug: Try to list what's in the array anyway
|
|
||||||
if [[ "${#NEW_LABELS_CREATED[@]}" -gt 0 ]] 2>/dev/null; then
|
|
||||||
for key in "${!NEW_LABELS_CREATED[@]}"; do
|
|
||||||
log_debug " Found key: $key"
|
|
||||||
done
|
|
||||||
else
|
|
||||||
log_debug " Array iteration failed - truly empty"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ $new_labels_count -eq 0 ]]; then
|
|
||||||
log_debug "No new labels created - skipping auto-update"
|
|
||||||
return 0 # No new labels, no update needed
|
|
||||||
fi
|
|
||||||
|
|
||||||
log_info "🔄 Auto-updating scripts with $new_labels_count new labels..."
|
|
||||||
|
|
||||||
# Check if update script exists
|
|
||||||
local update_script="$SCRIPT_DIR/update_script_labels.sh"
|
|
||||||
if [[ ! -f "$update_script" ]]; then
|
|
||||||
log_warning "Update script not found: $update_script"
|
|
||||||
log_warning "Skipping auto-update"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ ! -x "$update_script" ]]; then
|
|
||||||
log_warning "Update script not executable: $update_script"
|
|
||||||
log_warning "Making executable..."
|
|
||||||
chmod +x "$update_script"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Add new labels to registry
|
|
||||||
for label_name in "${!NEW_LABELS_CREATED[@]}"; do
|
|
||||||
local label_info="${NEW_LABELS_CREATED[$label_name]}"
|
|
||||||
local color=$(echo "$label_info" | cut -d':' -f1)
|
|
||||||
local context=$(echo "$label_info" | cut -d':' -f2)
|
|
||||||
local usage=$(echo "$label_info" | cut -d':' -f3)
|
|
||||||
|
|
||||||
log_info "Adding '$label_name' to registry..."
|
|
||||||
|
|
||||||
# Add to registry (suppressing output to avoid noise)
|
|
||||||
FURT_AUTO_UPDATE=true "$update_script" add "$label_name" "$color" "$context" "$usage" >/dev/null 2>&1 || {
|
|
||||||
log_warning "Failed to add $label_name to registry"
|
|
||||||
}
|
|
||||||
done
|
|
||||||
|
|
||||||
# Update all scripts with new labels
|
|
||||||
log_info "Synchronizing all scripts..."
|
|
||||||
"$update_script" update >/dev/null 2>&1 || {
|
|
||||||
log_warning "Failed to update scripts"
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
log_success "✅ All scripts automatically synchronized with new labels!"
|
|
||||||
|
|
||||||
# Show what was added
|
|
||||||
echo ""
|
|
||||||
echo "🆕 New labels created and synchronized:"
|
|
||||||
for label_name in "${!NEW_LABELS_CREATED[@]}"; do
|
|
||||||
echo " - $label_name (ID: ${LABEL_IDS[$label_name]})"
|
|
||||||
done
|
|
||||||
echo ""
|
|
||||||
}
|
|
||||||
|
|
||||||
# Create issue templates
|
|
||||||
create_service_issue() {
|
|
||||||
local service_name="${1:-newsletter}"
|
|
||||||
|
|
||||||
log_debug "Creating service issue for: $service_name"
|
|
||||||
|
|
||||||
local title="[SERVICE] $service_name für Furt Gateway"
|
|
||||||
local body="# Service-Request: $service_name
|
|
||||||
|
|
||||||
## 🏷️ Service-Details
|
|
||||||
**Name:** $service_name
|
|
||||||
**Port:** TBD
|
|
||||||
**Zweck:** [Service-Beschreibung]
|
|
||||||
|
|
||||||
## 📝 Funktionsanforderungen
|
|
||||||
- [ ] [Anforderung 1]
|
|
||||||
- [ ] [Anforderung 2]
|
|
||||||
- [ ] [Anforderung 3]
|
|
||||||
|
|
||||||
## 🔗 Gateway-Integration
|
|
||||||
- [ ] **Routing:** \`/v1/$service_name/*\`
|
|
||||||
- [ ] **Auth:** API-Key required
|
|
||||||
- [ ] **Rate-Limiting:** TBD req/min
|
|
||||||
- [ ] **Health-Check:** \`/health\`
|
|
||||||
|
|
||||||
## 🎯 Hugo-Integration
|
|
||||||
- [ ] **Shortcode:** \`{{< furt-$service_name >}}\`
|
|
||||||
- [ ] **JavaScript-Client**
|
|
||||||
- [ ] **CSS-Styling**
|
|
||||||
|
|
||||||
## ⚡ Priorität
|
|
||||||
🔥 **Hoch** - benötigt für Website-Launch"
|
|
||||||
|
|
||||||
# Process labels first, then build JSON
|
|
||||||
local service_label="service-$service_name"
|
|
||||||
log_debug "Service label to add: $service_label"
|
|
||||||
|
|
||||||
# First: Process all labels (this updates global arrays)
|
|
||||||
process_labels_for_template "service" "$service_label"
|
|
||||||
|
|
||||||
# Then: Build JSON from already-processed labels (pure function, no side effects)
|
|
||||||
local labels_json=$(build_labels_json_from_processed "service" "$service_label")
|
|
||||||
|
|
||||||
# Show which labels are being used (AFTER processing when labels are actually created)
|
|
||||||
show_labels_used "service" "$service_label"
|
|
||||||
|
|
||||||
create_issue "$title" "$body" "$labels_json"
|
|
||||||
}
|
|
||||||
|
|
||||||
create_architecture_issue() {
|
|
||||||
local topic="${1:-middleware-optimization}"
|
|
||||||
|
|
||||||
local title="[ARCH] Gateway $topic"
|
|
||||||
local body="# Architektur-Diskussion: $topic
|
|
||||||
|
|
||||||
## 🎯 Architektur-Thema
|
|
||||||
[Beschreibung des Architektur-Themas]
|
|
||||||
|
|
||||||
## 📊 Aktuelle Situation
|
|
||||||
- [Status Quo 1]
|
|
||||||
- [Status Quo 2]
|
|
||||||
|
|
||||||
## 💡 Vorgeschlagene Änderung
|
|
||||||
- [Vorschlag 1]
|
|
||||||
- [Vorschlag 2]
|
|
||||||
|
|
||||||
## 🔄 Alternativen
|
|
||||||
1. **Option A:** [Beschreibung]
|
|
||||||
2. **Option B:** [Beschreibung]
|
|
||||||
|
|
||||||
## 📈 Betroffene Bereiche
|
|
||||||
- [ ] Gateway-Performance
|
|
||||||
- [ ] Service-Integration
|
|
||||||
- [ ] Security
|
|
||||||
- [ ] Configuration-Management"
|
|
||||||
|
|
||||||
# Process labels first, then build JSON
|
|
||||||
process_labels_for_template "architecture"
|
|
||||||
local labels_json=$(build_labels_json_from_processed "architecture")
|
|
||||||
|
|
||||||
create_issue "$title" "$body" "$labels_json"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Generic template creator
|
|
||||||
create_generic_issue() {
|
|
||||||
local template="$1"
|
|
||||||
local component="${2:-gateway}"
|
|
||||||
local description="[Beschreibung hinzufügen]"
|
|
||||||
|
|
||||||
# Safe parameter handling for $3
|
|
||||||
if [[ $# -ge 3 ]] && [[ -n "${3:-}" ]]; then
|
|
||||||
description="$3"
|
|
||||||
fi
|
|
||||||
|
|
||||||
log_debug "Creating $template issue for: $component"
|
|
||||||
|
|
||||||
local title_prefix
|
|
||||||
case "$template" in
|
|
||||||
api) title_prefix="[API]" ;;
|
|
||||||
security) title_prefix="[SEC]" ;;
|
|
||||||
hugo) title_prefix="[HUGO]" ;;
|
|
||||||
deployment) title_prefix="[DEPLOY]" ;;
|
|
||||||
bug) title_prefix="[BUG]" ;;
|
|
||||||
*) title_prefix="[${template^^}]" ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
local title="$title_prefix $component $(echo ${template^} | sed 's/api/API Contract/')"
|
|
||||||
local body="# ${template^}: $component
|
|
||||||
|
|
||||||
## 📝 ${template^}-Details
|
|
||||||
**Komponente:** $component
|
|
||||||
**Beschreibung:** $description
|
|
||||||
|
|
||||||
## 🎯 Anforderungen
|
|
||||||
- [ ] [Anforderung 1]
|
|
||||||
- [ ] [Anforderung 2]
|
|
||||||
- [ ] [Anforderung 3]
|
|
||||||
|
|
||||||
## ✅ Definition of Done
|
|
||||||
- [ ] [DoD Kriterium 1]
|
|
||||||
- [ ] [DoD Kriterium 2]
|
|
||||||
- [ ] [DoD Kriterium 3]"
|
|
||||||
|
|
||||||
# Process labels first, then build JSON
|
|
||||||
process_labels_for_template "$template"
|
|
||||||
local labels_json=$(build_labels_json_from_processed "$template")
|
|
||||||
|
|
||||||
show_labels_used "$template"
|
|
||||||
create_issue "$title" "$body" "$labels_json"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Show usage information
|
|
||||||
show_usage() {
|
|
||||||
echo "🎯 Furt API-Gateway Issue Creator (Debug Version)"
|
|
||||||
echo ""
|
|
||||||
echo "Usage: $0 [TEMPLATE] [OPTIONS]"
|
|
||||||
echo ""
|
|
||||||
echo "📋 Available Templates:"
|
|
||||||
echo " service [name] New service for gateway (default: newsletter)"
|
|
||||||
echo " architecture [topic] Gateway architecture discussion (default: middleware-optimization)"
|
|
||||||
echo " performance [comp] Performance optimization (default: gateway)"
|
|
||||||
echo " api [service] API contract update (default: formular2mail)"
|
|
||||||
echo " security [comp] Security review/issue (default: gateway)"
|
|
||||||
echo " bug [comp] [desc] Bug report (default: gateway)"
|
|
||||||
echo " hugo [feature] Hugo integration (default: shortcode)"
|
|
||||||
echo " deployment [comp] Deployment issue (default: gateway)"
|
|
||||||
echo " custom Custom issue (interactive)"
|
|
||||||
echo ""
|
|
||||||
echo "🚀 Examples:"
|
|
||||||
echo " $0 service newsletter # Create newsletter service request"
|
|
||||||
echo " $0 architecture rate-limiting # Discuss rate limiting architecture"
|
|
||||||
echo " $0 performance gateway # Gateway performance optimization"
|
|
||||||
echo " $0 custom # Interactive custom issue"
|
|
||||||
echo ""
|
|
||||||
echo "🔧 Debug Mode:"
|
|
||||||
echo " Set DEBUG=1 for verbose debug output"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Generic issue creation
|
|
||||||
create_issue() {
|
|
||||||
local title="$1"
|
|
||||||
local body="$2"
|
|
||||||
local labels_json="$3"
|
|
||||||
|
|
||||||
log_info "Creating issue: $title"
|
|
||||||
log_debug "Labels JSON: $labels_json"
|
|
||||||
|
|
||||||
local response=$(curl -s -w "\n%{http_code}" -X POST \
|
|
||||||
"$GITEA_URL/api/v1/repos/$REPO_OWNER/$REPO_NAME/issues" \
|
|
||||||
-H "Authorization: token $GITEA_TOKEN" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d "{
|
|
||||||
\"title\": $(echo "$title" | jq -R .),
|
|
||||||
\"body\": $(echo "$body" | jq -R -s .),
|
|
||||||
\"labels\": $labels_json
|
|
||||||
}")
|
|
||||||
|
|
||||||
local http_code=$(echo "$response" | tail -n1)
|
|
||||||
local response_body=$(echo "$response" | head -n -1)
|
|
||||||
|
|
||||||
if [[ "$http_code" == "201" ]]; then
|
|
||||||
local issue_number=$(echo "$response_body" | jq -r '.number')
|
|
||||||
local issue_url=$(echo "$response_body" | jq -r '.html_url')
|
|
||||||
|
|
||||||
log_success "Issue #$issue_number created!"
|
|
||||||
echo "🔗 $issue_url"
|
|
||||||
else
|
|
||||||
log_error "Failed to create issue (HTTP: $http_code)"
|
|
||||||
log_error "Response: $response_body"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Main function
|
|
||||||
main() {
|
|
||||||
local template="${1:-help}"
|
|
||||||
|
|
||||||
# Enable debug if requested
|
|
||||||
if [[ "${DEBUG:-}" == "1" ]]; then
|
|
||||||
log_info "Debug mode enabled"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "$template" == "help" ]] || [[ "$template" == "--help" ]] || [[ "$template" == "-h" ]]; then
|
|
||||||
show_usage
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Load existing labels
|
|
||||||
load_existing_labels
|
|
||||||
|
|
||||||
case "$template" in
|
|
||||||
service)
|
|
||||||
create_service_issue "${2:-newsletter}"
|
|
||||||
;;
|
|
||||||
architecture)
|
|
||||||
create_architecture_issue "${2:-middleware-optimization}"
|
|
||||||
;;
|
|
||||||
performance)
|
|
||||||
local component="${2:-gateway}"
|
|
||||||
local title="[PERF] $component Performance-Optimierung"
|
|
||||||
local body="# Performance-Optimierung: $component"
|
|
||||||
|
|
||||||
process_labels_for_template "performance"
|
|
||||||
local labels_json=$(build_labels_json_from_processed "performance")
|
|
||||||
|
|
||||||
create_issue "$title" "$body" "$labels_json"
|
|
||||||
;;
|
|
||||||
api|security|hugo|deployment|bug)
|
|
||||||
if [[ $# -ge 3 ]]; then
|
|
||||||
create_generic_issue "$template" "${2:-gateway}" "$3"
|
|
||||||
else
|
|
||||||
create_generic_issue "$template" "${2:-gateway}"
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
log_error "Unknown template: $template"
|
|
||||||
show_usage
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
# FIXED: AUTO-UPDATE: Automatically sync scripts if new labels were created
|
|
||||||
auto_update_scripts_if_needed
|
|
||||||
}
|
|
||||||
|
|
||||||
# Run if executed directly
|
|
||||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
|
||||||
main "$@"
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
|
@ -1,250 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Load environment
|
|
||||||
if [ -f .env ]; then
|
|
||||||
export $(cat .env | grep -v '^#' | xargs)
|
|
||||||
else
|
|
||||||
echo "❌ .env file not found!"
|
|
||||||
echo "📋 Copy .env.example to .env and configure it first"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Validate required variables
|
|
||||||
if [ -z "$GITEA_URL" ] || [ -z "$REPO_OWNER" ] || [ -z "$REPO_NAME" ] || [ -z "$GITEA_TOKEN" ]; then
|
|
||||||
echo "❌ Missing required environment variables in .env"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Colors
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
BLUE='\033[0;34m'
|
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
RED='\033[0;31m'
|
|
||||||
NC='\033[0m'
|
|
||||||
|
|
||||||
log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
|
|
||||||
log_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
|
|
||||||
log_warning() { echo -e "${YELLOW}[WARNING]${NC} $1"; }
|
|
||||||
|
|
||||||
# Get all issues with nice formatting
|
|
||||||
get_all_issues() {
|
|
||||||
log_info "Fetching all issues..."
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
response=$(curl -s "$GITEA_URL/api/v1/repos/$REPO_OWNER/$REPO_NAME/issues" \
|
|
||||||
-H "Authorization: token $GITEA_TOKEN")
|
|
||||||
|
|
||||||
if [ $? -ne 0 ]; then
|
|
||||||
echo "❌ Error fetching issues"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "$response" | jq -r '.[] |
|
|
||||||
"🎯 #\(.number) \(.title)",
|
|
||||||
" 📊 State: \(.state) | 🏷️ Labels: \(.labels | map(.name) | join(", ") // "none")",
|
|
||||||
" 🔗 \(.html_url)",
|
|
||||||
""'
|
|
||||||
}
|
|
||||||
|
|
||||||
# Get issues by label
|
|
||||||
get_issues_by_label() {
|
|
||||||
local label="$1"
|
|
||||||
log_info "Fetching issues with label: $label"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
response=$(curl -s "$GITEA_URL/api/v1/repos/$REPO_OWNER/$REPO_NAME/issues?labels=$label" \
|
|
||||||
-H "Authorization: token $GITEA_TOKEN")
|
|
||||||
|
|
||||||
echo "$response" | jq -r '.[] |
|
|
||||||
"🎯 #\(.number) \(.title)",
|
|
||||||
" 📊 \(.state) | 🔗 \(.html_url)",
|
|
||||||
""'
|
|
||||||
}
|
|
||||||
|
|
||||||
# Get issue details
|
|
||||||
get_issue_details() {
|
|
||||||
local issue_number="$1"
|
|
||||||
log_info "Fetching details for issue #$issue_number"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
response=$(curl -s "$GITEA_URL/api/v1/repos/$REPO_OWNER/$REPO_NAME/issues/$issue_number" \
|
|
||||||
-H "Authorization: token $GITEA_TOKEN")
|
|
||||||
|
|
||||||
echo "$response" | jq -r '
|
|
||||||
"🎯 Issue #\(.number): \(.title)",
|
|
||||||
"📊 State: \(.state)",
|
|
||||||
"👤 Assignees: \(.assignees | map(.login) | join(", ") // "none")",
|
|
||||||
"🏷️ Labels: \(.labels | map(.name) | join(", ") // "none")",
|
|
||||||
"📅 Created: \(.created_at)",
|
|
||||||
"🔗 URL: \(.html_url)",
|
|
||||||
"",
|
|
||||||
"📝 Body:",
|
|
||||||
"\(.body // "No description")",
|
|
||||||
""'
|
|
||||||
}
|
|
||||||
|
|
||||||
# Close issue
|
|
||||||
close_issue() {
|
|
||||||
local issue_number="$1"
|
|
||||||
log_info "Closing issue #$issue_number"
|
|
||||||
|
|
||||||
response=$(curl -s -w "\n%{http_code}" -X PATCH \
|
|
||||||
"$GITEA_URL/api/v1/repos/$REPO_OWNER/$REPO_NAME/issues/$issue_number" \
|
|
||||||
-H "Authorization: token $GITEA_TOKEN" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"state": "closed"}')
|
|
||||||
|
|
||||||
http_code=$(echo "$response" | tail -n1)
|
|
||||||
if [ "$http_code" = "201" ]; then
|
|
||||||
log_success "Issue #$issue_number closed"
|
|
||||||
else
|
|
||||||
echo "❌ Failed to close issue (HTTP: $http_code)"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Get pipeline status (issues grouped by Kanban columns for Furt)
|
|
||||||
get_pipeline_status() {
|
|
||||||
log_info "Furt API Gateway - Pipeline Status Overview"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
echo "🔧 SERVICE REQUESTS:"
|
|
||||||
get_issues_by_label "service-request" | head -10
|
|
||||||
|
|
||||||
echo "🏗️ ARCHITECTURE DISCUSSIONS:"
|
|
||||||
get_issues_by_label "architecture"
|
|
||||||
|
|
||||||
echo "🚀 PERFORMANCE OPTIMIZATIONS:"
|
|
||||||
get_issues_by_label "performance"
|
|
||||||
|
|
||||||
echo "🔒 SECURITY REVIEWS:"
|
|
||||||
get_issues_by_label "security"
|
|
||||||
|
|
||||||
echo "🐛 BUGS:"
|
|
||||||
get_issues_by_label "bug"
|
|
||||||
|
|
||||||
echo "🌐 HUGO INTEGRATIONS:"
|
|
||||||
get_issues_by_label "hugo-integration"
|
|
||||||
|
|
||||||
echo "📋 WORK IN PROGRESS:"
|
|
||||||
get_issues_by_label "enhancement" | head -5
|
|
||||||
}
|
|
||||||
|
|
||||||
# Issue statistics
|
|
||||||
get_stats() {
|
|
||||||
log_info "Furt API Gateway - Issue Statistics"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
all_issues=$(curl -s "$GITEA_URL/api/v1/repos/$REPO_OWNER/$REPO_NAME/issues" \
|
|
||||||
-H "Authorization: token $GITEA_TOKEN")
|
|
||||||
|
|
||||||
total=$(echo "$all_issues" | jq length)
|
|
||||||
open=$(echo "$all_issues" | jq '[.[] | select(.state == "open")] | length')
|
|
||||||
closed=$(echo "$all_issues" | jq '[.[] | select(.state == "closed")] | length')
|
|
||||||
|
|
||||||
echo "📊 Total Issues: $total"
|
|
||||||
echo "✅ Open: $open"
|
|
||||||
echo "🔒 Closed: $closed"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
echo "🏷️ Furt Labels:"
|
|
||||||
echo "$all_issues" | jq -r '[.[] | .labels[].name] | group_by(.) | map({label: .[0], count: length}) | sort_by(.count) | reverse | limit(10; .[]) | " \(.label): \(.count)"'
|
|
||||||
}
|
|
||||||
|
|
||||||
case "${1:-help}" in
|
|
||||||
"all"|"")
|
|
||||||
get_all_issues
|
|
||||||
;;
|
|
||||||
"gateway")
|
|
||||||
get_issues_by_label "gateway"
|
|
||||||
;;
|
|
||||||
"service-request")
|
|
||||||
get_issues_by_label "service-request"
|
|
||||||
;;
|
|
||||||
"service-formular2mail")
|
|
||||||
get_issues_by_label "service-formular2mail"
|
|
||||||
;;
|
|
||||||
"service-sagjan")
|
|
||||||
get_issues_by_label "service-sagjan"
|
|
||||||
;;
|
|
||||||
"architecture")
|
|
||||||
get_issues_by_label "architecture"
|
|
||||||
;;
|
|
||||||
"performance")
|
|
||||||
get_issues_by_label "performance"
|
|
||||||
;;
|
|
||||||
"security")
|
|
||||||
get_issues_by_label "security"
|
|
||||||
;;
|
|
||||||
"bug")
|
|
||||||
get_issues_by_label "bug"
|
|
||||||
;;
|
|
||||||
"enhancement")
|
|
||||||
get_issues_by_label "enhancement"
|
|
||||||
;;
|
|
||||||
"hugo")
|
|
||||||
get_issues_by_label "hugo-integration"
|
|
||||||
;;
|
|
||||||
"deployment")
|
|
||||||
get_issues_by_label "deployment"
|
|
||||||
;;
|
|
||||||
"testing")
|
|
||||||
get_issues_by_label "testing"
|
|
||||||
;;
|
|
||||||
"documentation")
|
|
||||||
get_issues_by_label "documentation"
|
|
||||||
;;
|
|
||||||
"pipeline")
|
|
||||||
get_pipeline_status
|
|
||||||
;;
|
|
||||||
"stats")
|
|
||||||
get_stats
|
|
||||||
;;
|
|
||||||
"close")
|
|
||||||
if [ -z "$2" ]; then
|
|
||||||
echo "Usage: $0 close ISSUE_NUMBER"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
close_issue "$2"
|
|
||||||
;;
|
|
||||||
[0-9]*)
|
|
||||||
get_issue_details "$1"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "🎯 Furt API Gateway - Issues Manager"
|
|
||||||
echo ""
|
|
||||||
echo "Usage: $0 [COMMAND] [OPTIONS]"
|
|
||||||
echo ""
|
|
||||||
echo "📋 List Commands:"
|
|
||||||
echo " all List all issues (default)"
|
|
||||||
echo " gateway Gateway core issues"
|
|
||||||
echo " service-request New service requests"
|
|
||||||
echo " service-formular2mail Formular2mail service issues"
|
|
||||||
echo " service-sagjan Sagjan service issues"
|
|
||||||
echo " architecture Architecture discussions"
|
|
||||||
echo " performance Performance optimizations"
|
|
||||||
echo " security Security reviews"
|
|
||||||
echo " bug Bug reports"
|
|
||||||
echo " enhancement New features"
|
|
||||||
echo " hugo Hugo integration issues"
|
|
||||||
echo " deployment Deployment issues"
|
|
||||||
echo " testing Testing issues"
|
|
||||||
echo " documentation Documentation updates"
|
|
||||||
echo ""
|
|
||||||
echo "📊 Analysis Commands:"
|
|
||||||
echo " pipeline Kanban pipeline status"
|
|
||||||
echo " stats Issue statistics"
|
|
||||||
echo ""
|
|
||||||
echo "⚙️ Management Commands:"
|
|
||||||
echo " close NUM Close issue #NUM"
|
|
||||||
echo " NUM Show details for issue #NUM"
|
|
||||||
echo ""
|
|
||||||
echo "🚀 Examples:"
|
|
||||||
echo " $0 # List all issues"
|
|
||||||
echo " $0 pipeline # Show pipeline status"
|
|
||||||
echo " $0 service-request # Show service requests"
|
|
||||||
echo " $0 gateway # Show gateway issues"
|
|
||||||
echo " $0 5 # Show issue #5 details"
|
|
||||||
echo " $0 close 3 # Close issue #3"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
|
|
@ -1,156 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Load environment
|
|
||||||
if [ -f .env ]; then
|
|
||||||
export $(cat .env | grep -v '^#' | xargs)
|
|
||||||
else
|
|
||||||
echo "❌ .env file not found!"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Colors
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
BLUE='\033[0;34m'
|
|
||||||
RED='\033[0;31m'
|
|
||||||
NC='\033[0m'
|
|
||||||
|
|
||||||
log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
|
|
||||||
log_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
|
|
||||||
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
|
||||||
|
|
||||||
# Get all labels with IDs
|
|
||||||
declare -A LABEL_IDS
|
|
||||||
get_labels() {
|
|
||||||
response=$(curl -s "$GITEA_URL/api/v1/repos/$REPO_OWNER/$REPO_NAME/labels" \
|
|
||||||
-H "Authorization: token $GITEA_TOKEN")
|
|
||||||
while IFS= read -r line; do
|
|
||||||
name=$(echo "$line" | jq -r '.name')
|
|
||||||
id=$(echo "$line" | jq -r '.id')
|
|
||||||
LABEL_IDS["$name"]="$id"
|
|
||||||
done < <(echo "$response" | jq -c '.[]')
|
|
||||||
}
|
|
||||||
|
|
||||||
# Add comment to issue
|
|
||||||
add_comment() {
|
|
||||||
local issue_number="$1"
|
|
||||||
local comment="$2"
|
|
||||||
|
|
||||||
# Use jq for proper JSON escaping
|
|
||||||
local json_payload=$(jq -n --arg body "$comment" '{body: $body}')
|
|
||||||
|
|
||||||
response=$(curl -s -w "\n%{http_code}" -X POST \
|
|
||||||
"$GITEA_URL/api/v1/repos/$REPO_OWNER/$REPO_NAME/issues/$issue_number/comments" \
|
|
||||||
-H "Authorization: token $GITEA_TOKEN" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d "$json_payload")
|
|
||||||
|
|
||||||
http_code=$(echo "$response" | tail -n1)
|
|
||||||
if [ "$http_code" = "201" ]; then
|
|
||||||
log_success "Comment added to issue #$issue_number"
|
|
||||||
else
|
|
||||||
log_error "Failed to add comment (HTTP: $http_code)"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Update issue labels - FIXED VERSION
|
|
||||||
update_labels() {
|
|
||||||
local issue_number="$1"
|
|
||||||
local labels_string="$2"
|
|
||||||
|
|
||||||
get_labels
|
|
||||||
|
|
||||||
# Convert to ID array
|
|
||||||
local valid_label_ids=()
|
|
||||||
IFS=',' read -ra LABEL_ARRAY <<< "$labels_string"
|
|
||||||
|
|
||||||
for label in "${LABEL_ARRAY[@]}"; do
|
|
||||||
label=$(echo "$label" | xargs)
|
|
||||||
if [ -n "${LABEL_IDS[$label]}" ]; then
|
|
||||||
valid_label_ids+=("${LABEL_IDS[$label]}")
|
|
||||||
else
|
|
||||||
log_error "Label '$label' not found!"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
# Build ID array JSON
|
|
||||||
local labels_json="["
|
|
||||||
for i in "${!valid_label_ids[@]}"; do
|
|
||||||
if [ $i -gt 0 ]; then
|
|
||||||
labels_json="${labels_json},"
|
|
||||||
fi
|
|
||||||
labels_json="${labels_json}${valid_label_ids[$i]}"
|
|
||||||
done
|
|
||||||
labels_json="${labels_json}]"
|
|
||||||
|
|
||||||
response=$(curl -s -w "\n%{http_code}" -X PUT \
|
|
||||||
"$GITEA_URL/api/v1/repos/$REPO_OWNER/$REPO_NAME/issues/$issue_number/labels" \
|
|
||||||
-H "Authorization: token $GITEA_TOKEN" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d "$labels_json")
|
|
||||||
|
|
||||||
http_code=$(echo "$response" | tail -n1)
|
|
||||||
if [ "$http_code" = "200" ]; then
|
|
||||||
log_success "Labels updated for issue #$issue_number"
|
|
||||||
else
|
|
||||||
log_error "Failed to update labels (HTTP: $http_code)"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
case "${1:-help}" in
|
|
||||||
"comment")
|
|
||||||
if [ -z "$2" ] || [ -z "$3" ]; then
|
|
||||||
echo "Usage: $0 comment ISSUE_NUMBER \"COMMENT_TEXT\""
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
add_comment "$2" "$3"
|
|
||||||
;;
|
|
||||||
"labels")
|
|
||||||
if [ -z "$2" ] || [ -z "$3" ]; then
|
|
||||||
echo "Usage: $0 labels ISSUE_NUMBER \"label1,label2,label3\""
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
update_labels "$2" "$3"
|
|
||||||
;;
|
|
||||||
"progress")
|
|
||||||
if [ -z "$2" ]; then
|
|
||||||
echo "Usage: $0 progress ISSUE_NUMBER"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
add_comment "$2" "📊 **Progress Update:** Arbeit an dieser Analyse läuft. Erste Quellen werden gesammelt und Framework-Relevanz geprüft."
|
|
||||||
update_labels "$2" "work-in-progress"
|
|
||||||
;;
|
|
||||||
"review")
|
|
||||||
if [ -z "$2" ]; then
|
|
||||||
echo "Usage: $0 review ISSUE_NUMBER"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
add_comment "$2" "👀 **Ready for Review:** Erste Analyse abgeschlossen. Bitte um Peer-Review der Quellen und Framework-Integration."
|
|
||||||
update_labels "$2" "needs-review"
|
|
||||||
;;
|
|
||||||
"fact-check")
|
|
||||||
if [ -z "$2" ]; then
|
|
||||||
echo "Usage: $0 fact-check ISSUE_NUMBER"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
add_comment "$2" "🔍 **Fact-Check Required:** Kritische Behauptungen gefunden die zusätzliche Quellen-Verifikation benötigen."
|
|
||||||
update_labels "$2" "fact-check-needed"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "🔧 Issue Update Tool (FIXED VERSION)"
|
|
||||||
echo ""
|
|
||||||
echo "Usage: $0 COMMAND ISSUE_NUMBER [OPTIONS]"
|
|
||||||
echo ""
|
|
||||||
echo "Commands:"
|
|
||||||
echo " comment NUM \"TEXT\" Add comment to issue"
|
|
||||||
echo " labels NUM \"l1,l2\" Update issue labels (using IDs)"
|
|
||||||
echo " progress NUM Mark as work-in-progress"
|
|
||||||
echo " review NUM Mark as ready for review"
|
|
||||||
echo " fact-check NUM Mark as needing fact-check"
|
|
||||||
echo ""
|
|
||||||
echo "Examples:"
|
|
||||||
echo " $0 comment 5 \"Erste Quellen gefunden\""
|
|
||||||
echo " $0 labels 3 \"regional-case,work-in-progress\""
|
|
||||||
echo " $0 progress 7"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
@ -1,491 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
# scripts/update_script_labels.sh
|
|
||||||
# Auto-updates all scripts with current label definitions from registry
|
|
||||||
# FINAL FIXED VERSION with corrected all_templates logic
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
|
|
||||||
REGISTRY_FILE="$PROJECT_ROOT/configs/labels.registry"
|
|
||||||
|
|
||||||
# Colors for logging
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
BLUE='\033[0;34m'
|
|
||||||
RED='\033[0;31m'
|
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
NC='\033[0m'
|
|
||||||
|
|
||||||
log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
|
|
||||||
log_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
|
|
||||||
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
|
||||||
log_warning() { echo -e "${YELLOW}[WARNING]${NC} $1"; }
|
|
||||||
|
|
||||||
# Parse label registry into associative arrays
|
|
||||||
declare -A LABEL_COLORS
|
|
||||||
declare -A LABEL_CONTEXTS
|
|
||||||
declare -A LABEL_USAGES
|
|
||||||
|
|
||||||
# Create registry file if it doesn't exist
|
|
||||||
create_registry_if_missing() {
|
|
||||||
if [[ ! -f "$REGISTRY_FILE" ]]; then
|
|
||||||
log_info "Creating label registry file..."
|
|
||||||
|
|
||||||
mkdir -p "$(dirname "$REGISTRY_FILE")"
|
|
||||||
|
|
||||||
cat > "$REGISTRY_FILE" << 'EOF'
|
|
||||||
# Central Label Registry for Furt API Gateway Project
|
|
||||||
# Format: name:color:context:usage_contexts
|
|
||||||
# This file is the single source of truth for all labels
|
|
||||||
|
|
||||||
# === CORE WORKFLOW LABELS ===
|
|
||||||
service-request:7057ff:new_service:service_templates,status_updates
|
|
||||||
enhancement:84b6eb:improvement:all_templates
|
|
||||||
bug:d73a4a:error:bug_template,status_updates
|
|
||||||
question:d876e3:discussion:question_template
|
|
||||||
|
|
||||||
# === COMPONENT CATEGORIES ===
|
|
||||||
gateway:0052cc:gateway_core:architecture_template,performance_template,service_templates
|
|
||||||
performance:fbca04:optimization:performance_template,architecture_template
|
|
||||||
architecture:d4c5f9:design:architecture_template,gateway
|
|
||||||
security:28a745:security_review:security_template,architecture_template
|
|
||||||
configuration:f9d71c:config_management:deployment_template,architecture_template
|
|
||||||
|
|
||||||
# === SERVICE-SPECIFIC LABELS ===
|
|
||||||
service-formular2mail:1d76db:formular2mail:formular2mail_integration
|
|
||||||
service-sagjan:1d76db:sagjan:sagjan_integration
|
|
||||||
service-newsletter:ff6b6b:newsletter:newsletter_integration
|
|
||||||
|
|
||||||
# === WORKFLOW STATE LABELS ===
|
|
||||||
work-in-progress:fbca04:active:status_updates
|
|
||||||
needs-review:0e8a16:review:status_updates
|
|
||||||
blocked:d73a4a:blocked:status_updates
|
|
||||||
ready-for-deployment:28a745:deploy_ready:status_updates
|
|
||||||
|
|
||||||
# === INTEGRATION LABELS ===
|
|
||||||
hugo-integration:ff7518:frontend:hugo_templates,integration
|
|
||||||
api-contract:5319e7:api_design:api_templates,service_templates
|
|
||||||
breaking-change:d73a4a:breaking:api_templates,architecture_template
|
|
||||||
|
|
||||||
# === PRIORITY LABELS ===
|
|
||||||
high-priority:d73a4a:urgent:all_templates
|
|
||||||
low-priority:0e8a16:nice_to_have:all_templates
|
|
||||||
|
|
||||||
# === META LABELS ===
|
|
||||||
low-tech:6f42c1:low_tech_principle:architecture_template,performance_template,security_template
|
|
||||||
digital-sovereignty:6f42c1:digital_sovereignty:architecture_template,performance_template,security_template
|
|
||||||
good-first-issue:7057ff:beginner_friendly:manual_assignment
|
|
||||||
help-wanted:159818:community_help:manual_assignment
|
|
||||||
|
|
||||||
# === DEPLOYMENT LABELS ===
|
|
||||||
deployment:ff7518:deployment:deployment_template
|
|
||||||
testing:f9d71c:testing:testing_template,integration
|
|
||||||
EOF
|
|
||||||
|
|
||||||
log_success "Created label registry: $REGISTRY_FILE"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
parse_registry() {
|
|
||||||
create_registry_if_missing
|
|
||||||
|
|
||||||
log_info "Parsing label registry..."
|
|
||||||
|
|
||||||
while IFS= read -r line; do
|
|
||||||
# Skip comments and empty lines
|
|
||||||
[[ "$line" =~ ^#.*$ ]] && continue
|
|
||||||
[[ -z "$line" ]] && continue
|
|
||||||
|
|
||||||
# Parse format: name:color:context:usage_contexts
|
|
||||||
if [[ "$line" =~ ^([^:]+):([^:]+):([^:]+):(.+)$ ]]; then
|
|
||||||
local name="${BASH_REMATCH[1]}"
|
|
||||||
local color="${BASH_REMATCH[2]}"
|
|
||||||
local context="${BASH_REMATCH[3]}"
|
|
||||||
local usage="${BASH_REMATCH[4]}"
|
|
||||||
|
|
||||||
LABEL_COLORS["$name"]="$color"
|
|
||||||
LABEL_CONTEXTS["$name"]="$context"
|
|
||||||
LABEL_USAGES["$name"]="$usage"
|
|
||||||
|
|
||||||
log_info "Loaded label: $name ($context)"
|
|
||||||
fi
|
|
||||||
done < "$REGISTRY_FILE"
|
|
||||||
|
|
||||||
log_success "Loaded ${#LABEL_COLORS[@]} labels from registry"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Generate label definitions section for scripts
|
|
||||||
generate_label_definitions() {
|
|
||||||
cat << 'EOF'
|
|
||||||
# === LABEL DEFINITIONS START ===
|
|
||||||
# This section is auto-maintained by update_script_labels.sh
|
|
||||||
# DO NOT EDIT MANUALLY - Changes will be overwritten
|
|
||||||
declare -A LABEL_DEFINITIONS=(
|
|
||||||
EOF
|
|
||||||
|
|
||||||
for label in "${!LABEL_COLORS[@]}"; do
|
|
||||||
local color="${LABEL_COLORS[$label]}"
|
|
||||||
local context="${LABEL_CONTEXTS[$label]}"
|
|
||||||
local usage="${LABEL_USAGES[$label]}"
|
|
||||||
|
|
||||||
echo " [\"$label\"]=\"color:$color;context:$context;usage:$usage\""
|
|
||||||
done
|
|
||||||
|
|
||||||
cat << 'EOF'
|
|
||||||
)
|
|
||||||
|
|
||||||
# Extract label info
|
|
||||||
get_label_color() { echo "${LABEL_DEFINITIONS[$1]}" | cut -d';' -f1 | cut -d':' -f2; }
|
|
||||||
get_label_context() { echo "${LABEL_DEFINITIONS[$1]}" | cut -d';' -f2 | cut -d':' -f2; }
|
|
||||||
get_label_usage() { echo "${LABEL_DEFINITIONS[$1]}" | cut -d';' -f3 | cut -d':' -f2; }
|
|
||||||
|
|
||||||
# Check if label is valid for context
|
|
||||||
is_label_valid_for_context() {
|
|
||||||
local label="$1"
|
|
||||||
local context="$2"
|
|
||||||
local usage=$(get_label_usage "$label")
|
|
||||||
[[ "$usage" == *"$context"* ]] || [[ "$usage" == "all_templates" ]]
|
|
||||||
}
|
|
||||||
# === LABEL DEFINITIONS END ===
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
# Generate template-to-labels mapping - FIXED VERSION
|
|
||||||
generate_template_mappings() {
|
|
||||||
cat << 'EOF'
|
|
||||||
|
|
||||||
# === TEMPLATE LABEL MAPPINGS START ===
|
|
||||||
# Auto-generated template to label mappings
|
|
||||||
declare -A TEMPLATE_LABELS=(
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# FIXED: Consistent template names with corrected all_templates logic
|
|
||||||
declare -A template_mappings=(
|
|
||||||
["service"]="service_templates"
|
|
||||||
["architecture"]="architecture_template"
|
|
||||||
["performance"]="performance_template"
|
|
||||||
["bug"]="bug_template"
|
|
||||||
["security"]="security_template"
|
|
||||||
["hugo"]="hugo_templates"
|
|
||||||
["api"]="api_templates"
|
|
||||||
["deployment"]="deployment_template"
|
|
||||||
)
|
|
||||||
|
|
||||||
for template_name in "${!template_mappings[@]}"; do
|
|
||||||
local template_usage="${template_mappings[$template_name]}"
|
|
||||||
local labels=()
|
|
||||||
|
|
||||||
# FIXED: First add all_templates labels to every template
|
|
||||||
for label in "${!LABEL_USAGES[@]}"; do
|
|
||||||
local usage="${LABEL_USAGES[$label]}"
|
|
||||||
|
|
||||||
# EXCLUDE service-specific labels from all templates
|
|
||||||
if [[ "$label" == service-* ]]; then
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Skip manual assignment labels
|
|
||||||
if [[ "$usage" == "manual_assignment" ]]; then
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
# FIXED: Add all_templates labels to every template first
|
|
||||||
if [[ "$usage" == "all_templates" ]]; then
|
|
||||||
labels+=("$label")
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Add template-specific labels
|
|
||||||
if [[ "$usage" == *"$template_usage"* ]]; then
|
|
||||||
labels+=("$label")
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
if [[ ${#labels[@]} -gt 0 ]]; then
|
|
||||||
local label_list=$(IFS=','; echo "${labels[*]}")
|
|
||||||
echo " [\"$template_name\"]=\"$label_list\""
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
cat << 'EOF'
|
|
||||||
)
|
|
||||||
# === TEMPLATE LABEL MAPPINGS END ===
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
# Generate filter options for get_issues.sh
|
|
||||||
generate_filter_options() {
|
|
||||||
cat << 'EOF'
|
|
||||||
|
|
||||||
# === FILTER OPTIONS START ===
|
|
||||||
# Auto-generated filter options for get_issues.sh
|
|
||||||
show_filter_help() {
|
|
||||||
echo "📋 Available filters:"
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# Group labels by context for better help display
|
|
||||||
declare -A context_labels
|
|
||||||
for label in "${!LABEL_CONTEXTS[@]}"; do
|
|
||||||
local context="${LABEL_CONTEXTS[$label]}"
|
|
||||||
if [[ -z "${context_labels[$context]}" ]]; then
|
|
||||||
context_labels[$context]="$label"
|
|
||||||
else
|
|
||||||
context_labels[$context]="${context_labels[$context]},$label"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
for context in "${!context_labels[@]}"; do
|
|
||||||
echo " echo \" $context: ${context_labels[$context]}\""
|
|
||||||
done
|
|
||||||
|
|
||||||
cat << 'EOF'
|
|
||||||
}
|
|
||||||
|
|
||||||
# Filter case statement
|
|
||||||
handle_filter() {
|
|
||||||
local filter="$1"
|
|
||||||
case "$filter" in
|
|
||||||
EOF
|
|
||||||
|
|
||||||
for label in "${!LABEL_COLORS[@]}"; do
|
|
||||||
echo " $label) filter_by_label \"$label\" ;;"
|
|
||||||
done
|
|
||||||
|
|
||||||
cat << 'EOF'
|
|
||||||
pipeline) show_pipeline_overview ;;
|
|
||||||
stats) show_statistics ;;
|
|
||||||
all) show_all_issues ;;
|
|
||||||
*)
|
|
||||||
log_error "Unknown filter: $filter"
|
|
||||||
show_filter_help
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
# === FILTER OPTIONS END ===
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
# Update a single script file
|
|
||||||
update_script_file() {
|
|
||||||
local script_file="$1"
|
|
||||||
|
|
||||||
if [[ ! -f "$script_file" ]]; then
|
|
||||||
log_warning "Script not found: $script_file"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
log_info "Updating $script_file..."
|
|
||||||
|
|
||||||
# Create backup
|
|
||||||
cp "$script_file" "${script_file}.backup"
|
|
||||||
|
|
||||||
# Check if script has label definition sections
|
|
||||||
if ! grep -q "# === LABEL DEFINITIONS START ===" "$script_file"; then
|
|
||||||
log_warning "$script_file has no label definitions section - skipping"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Find section boundaries
|
|
||||||
local start_line=$(grep -n "# === LABEL DEFINITIONS START ===" "$script_file" | cut -d: -f1)
|
|
||||||
local end_line=$(grep -n "# === LABEL DEFINITIONS END ===" "$script_file" | cut -d: -f1)
|
|
||||||
|
|
||||||
if [[ -z "$start_line" ]] || [[ -z "$end_line" ]]; then
|
|
||||||
log_error "Malformed label definition section in $script_file"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Generate new content
|
|
||||||
local new_definitions=$(generate_label_definitions)
|
|
||||||
|
|
||||||
# Handle template mappings if present
|
|
||||||
if grep -q "# === TEMPLATE LABEL MAPPINGS START ===" "$script_file"; then
|
|
||||||
new_definitions+="\n$(generate_template_mappings)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Handle filter options if present (for get_issues.sh)
|
|
||||||
if grep -q "# === FILTER OPTIONS START ===" "$script_file"; then
|
|
||||||
new_definitions+="\n$(generate_filter_options)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Create temporary file with updated content
|
|
||||||
local temp_file=$(mktemp)
|
|
||||||
|
|
||||||
# Copy everything before label definitions
|
|
||||||
sed -n "1,$((start_line-1))p" "$script_file" > "$temp_file"
|
|
||||||
|
|
||||||
# Add new definitions
|
|
||||||
echo -e "$new_definitions" >> "$temp_file"
|
|
||||||
|
|
||||||
# Copy everything after label definitions (find new end line)
|
|
||||||
if grep -q "# === TEMPLATE LABEL MAPPINGS END ===" "$script_file"; then
|
|
||||||
local actual_end_line=$(grep -n "# === TEMPLATE LABEL MAPPINGS END ===" "$script_file" | cut -d: -f1)
|
|
||||||
elif grep -q "# === FILTER OPTIONS END ===" "$script_file"; then
|
|
||||||
local actual_end_line=$(grep -n "# === FILTER OPTIONS END ===" "$script_file" | cut -d: -f1)
|
|
||||||
else
|
|
||||||
local actual_end_line="$end_line"
|
|
||||||
fi
|
|
||||||
|
|
||||||
sed -n "$((actual_end_line+1)),\$p" "$script_file" >> "$temp_file"
|
|
||||||
|
|
||||||
# Replace original file
|
|
||||||
mv "$temp_file" "$script_file"
|
|
||||||
chmod +x "$script_file"
|
|
||||||
|
|
||||||
log_success "Updated $script_file"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Add new label to registry
|
|
||||||
add_label_to_registry() {
|
|
||||||
local name="$1"
|
|
||||||
local color="${2:-ff6b6b}"
|
|
||||||
local context="${3:-auto_generated}"
|
|
||||||
local usage="${4:-manual_assignment}"
|
|
||||||
|
|
||||||
# Skip if called during auto-update to prevent loops
|
|
||||||
if [[ "${FURT_AUTO_UPDATE:-}" == "true" ]]; then
|
|
||||||
log_info "Auto-update mode: Adding $name to registry (skipping rebuild)"
|
|
||||||
else
|
|
||||||
log_info "Adding new label to registry: $name"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Ensure registry exists
|
|
||||||
create_registry_if_missing
|
|
||||||
|
|
||||||
# Check if label already exists
|
|
||||||
if grep -q "^$name:" "$REGISTRY_FILE"; then
|
|
||||||
log_warning "Label $name already exists in registry"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Add to appropriate section (determine by context)
|
|
||||||
local section_marker="# === CORE WORKFLOW LABELS ==="
|
|
||||||
if [[ "$context" == *"service"* ]] || [[ "$name" == service-* ]]; then
|
|
||||||
section_marker="# === SERVICE-SPECIFIC LABELS ==="
|
|
||||||
elif [[ "$context" == *"workflow"* ]]; then
|
|
||||||
section_marker="# === WORKFLOW STATE LABELS ==="
|
|
||||||
elif [[ "$context" == *"component"* ]]; then
|
|
||||||
section_marker="# === COMPONENT CATEGORIES ==="
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Create backup
|
|
||||||
cp "$REGISTRY_FILE" "${REGISTRY_FILE}.backup"
|
|
||||||
|
|
||||||
# Find section and add label
|
|
||||||
local temp_file=$(mktemp)
|
|
||||||
local added=false
|
|
||||||
|
|
||||||
while IFS= read -r line; do
|
|
||||||
echo "$line" >> "$temp_file"
|
|
||||||
|
|
||||||
if [[ "$line" == "$section_marker" ]] && [[ "$added" == false ]]; then
|
|
||||||
echo "$name:$color:$context:$usage" >> "$temp_file"
|
|
||||||
added=true
|
|
||||||
log_success "Added label $name to registry"
|
|
||||||
fi
|
|
||||||
done < "$REGISTRY_FILE"
|
|
||||||
|
|
||||||
mv "$temp_file" "$REGISTRY_FILE"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Show current registry status
|
|
||||||
show_registry_status() {
|
|
||||||
echo "📊 Label Registry Status"
|
|
||||||
echo "========================"
|
|
||||||
echo "Registry file: $REGISTRY_FILE"
|
|
||||||
|
|
||||||
if [[ -f "$REGISTRY_FILE" ]]; then
|
|
||||||
echo "Total labels: $(grep -c "^[^#]" "$REGISTRY_FILE" 2>/dev/null || echo 0)"
|
|
||||||
echo ""
|
|
||||||
echo "Labels by category:"
|
|
||||||
|
|
||||||
local current_section=""
|
|
||||||
while IFS= read -r line; do
|
|
||||||
if [[ "$line" =~ ^#\ ===.*===\ $ ]]; then
|
|
||||||
current_section=$(echo "$line" | sed 's/# === \(.*\) ===/\1/')
|
|
||||||
echo " $current_section:"
|
|
||||||
elif [[ "$line" =~ ^[^#]+: ]] && [[ -n "$current_section" ]]; then
|
|
||||||
local label_name=$(echo "$line" | cut -d: -f1)
|
|
||||||
echo " - $label_name"
|
|
||||||
fi
|
|
||||||
done < "$REGISTRY_FILE"
|
|
||||||
else
|
|
||||||
echo "Registry file not found!"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Main function
|
|
||||||
main() {
|
|
||||||
local command="${1:-update}"
|
|
||||||
|
|
||||||
case "$command" in
|
|
||||||
update)
|
|
||||||
log_info "Starting label synchronization..."
|
|
||||||
parse_registry
|
|
||||||
|
|
||||||
# Update all script files
|
|
||||||
local scripts=(
|
|
||||||
"$PROJECT_ROOT/scripts/create_issue.sh"
|
|
||||||
"$PROJECT_ROOT/scripts/get_issues.sh"
|
|
||||||
"$PROJECT_ROOT/scripts/update_issue.sh"
|
|
||||||
)
|
|
||||||
|
|
||||||
for script in "${scripts[@]}"; do
|
|
||||||
update_script_file "$script"
|
|
||||||
done
|
|
||||||
|
|
||||||
log_success "All scripts synchronized with label registry!"
|
|
||||||
;;
|
|
||||||
|
|
||||||
add)
|
|
||||||
local name="$2"
|
|
||||||
local color="${3:-ff6b6b}"
|
|
||||||
local context="${4:-auto_generated}"
|
|
||||||
local usage="${5:-manual_assignment}"
|
|
||||||
|
|
||||||
if [[ -z "$name" ]]; then
|
|
||||||
log_error "Usage: $0 add <name> [color] [context] [usage]"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
add_label_to_registry "$name" "$color" "$context" "$usage"
|
|
||||||
|
|
||||||
# Only update scripts if not in auto-update mode
|
|
||||||
if [[ "${FURT_AUTO_UPDATE:-}" != "true" ]]; then
|
|
||||||
parse_registry
|
|
||||||
main update
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
|
|
||||||
status)
|
|
||||||
show_registry_status
|
|
||||||
;;
|
|
||||||
|
|
||||||
help)
|
|
||||||
echo "Usage: $0 [command] [options]"
|
|
||||||
echo ""
|
|
||||||
echo "Commands:"
|
|
||||||
echo " update Update all scripts with current registry"
|
|
||||||
echo " add <name> Add new label to registry and update scripts"
|
|
||||||
echo " status Show registry status"
|
|
||||||
echo " help Show this help"
|
|
||||||
echo ""
|
|
||||||
echo "Examples:"
|
|
||||||
echo " $0 update"
|
|
||||||
echo " $0 add newsletter ff6b6b newsletter_service service_templates"
|
|
||||||
echo " $0 status"
|
|
||||||
;;
|
|
||||||
|
|
||||||
*)
|
|
||||||
log_error "Unknown command: $command"
|
|
||||||
main help
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
# Run if executed directly
|
|
||||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
|
||||||
main "$@"
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
|
@ -1,966 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
# scripts/deploy/deploy_aitvaras.sh
|
|
||||||
# Deployment script: karl (development) → aitvaras (Ubuntu 24.04 production)
|
|
||||||
#
|
|
||||||
# Usage:
|
|
||||||
# ./scripts/deploy/deploy_aitvaras.sh [--dry-run] [--rollback] [--force]
|
|
||||||
#
|
|
||||||
# Dragons@Work - Furt API-Gateway Production Deployment
|
|
||||||
# Version: 1.0
|
|
||||||
|
|
||||||
set -euo pipefail # Exit on error, undefined vars, pipe failures
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# CONFIGURATION
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
# Source (karl development)
|
|
||||||
SOURCE_DIR="/home/michael/Develop/DAW/furt/furt-lua"
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
PROJECT_ROOT="$(dirname "$(dirname "$SCRIPT_DIR")")" # scripts/deploy/ -> scripts/ -> furt/
|
|
||||||
|
|
||||||
# Target (aitvaras Ubuntu production)
|
|
||||||
AITVARAS_HOST="aitvaras" # Assumes SSH config entry (as michael user)
|
|
||||||
TARGET_DIR="/opt/furt-api"
|
|
||||||
SERVICE_USER="_furt"
|
|
||||||
SERVICE_GROUP="_furt"
|
|
||||||
SERVICE_NAME="furt-api"
|
|
||||||
|
|
||||||
# Ubuntu-specific paths
|
|
||||||
CONFIG_DIR="/etc/furt" # ← Angepasst: wie start.sh erwartet
|
|
||||||
LOG_DIR="/var/log/furt-api"
|
|
||||||
RUN_DIR="/var/run/furt-api"
|
|
||||||
BACKUP_DIR="/var/backup/furt-api"
|
|
||||||
SYSTEMD_SERVICE="/etc/systemd/system/furt-api.service"
|
|
||||||
|
|
||||||
# Backup configuration
|
|
||||||
BACKUP_RETENTION=5 # Keep last 5 backups (production = more backups)
|
|
||||||
|
|
||||||
# Health check configuration
|
|
||||||
HEALTH_URL="http://localhost:8080/health"
|
|
||||||
HEALTH_TIMEOUT=15 # Longer timeout for production
|
|
||||||
HEALTH_RETRIES=5 # More retries for production
|
|
||||||
|
|
||||||
# Colors for output
|
|
||||||
RED='\033[0;31m'
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
BLUE='\033[0;34m'
|
|
||||||
PURPLE='\033[0;35m'
|
|
||||||
NC='\033[0m' # No Color
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# LOGGING FUNCTIONS
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
log_info() {
|
|
||||||
echo -e "${BLUE}[INFO]${NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
log_success() {
|
|
||||||
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
log_warning() {
|
|
||||||
echo -e "${YELLOW}[WARNING]${NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
log_error() {
|
|
||||||
echo -e "${RED}[ERROR]${NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
log_step() {
|
|
||||||
echo -e "\n${PURPLE}==>${NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
log_production() {
|
|
||||||
echo -e "${PURPLE}[PRODUCTION]${NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# UTILITY FUNCTIONS
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
cat << EOF
|
|
||||||
Usage: $0 [OPTIONS]
|
|
||||||
|
|
||||||
🚨 PRODUCTION deployment script for furt-api: karl → aitvaras (Ubuntu 24.04)
|
|
||||||
|
|
||||||
⚠️ WARNING: This deploys to PRODUCTION server aitvaras!
|
|
||||||
⚠️ All changes are immediately live and affect production services.
|
|
||||||
|
|
||||||
🔐 SECURITY NOTE:
|
|
||||||
This script temporarily enables passwordless sudo for deployment,
|
|
||||||
then automatically disables it for security.
|
|
||||||
|
|
||||||
OPTIONS:
|
|
||||||
--dry-run Show what would be deployed without making changes
|
|
||||||
--rollback Rollback to previous deployment
|
|
||||||
--force Skip confirmation prompts (USE WITH CAUTION!)
|
|
||||||
--help Show this help message
|
|
||||||
|
|
||||||
EXAMPLES:
|
|
||||||
$0 # Normal production deployment with confirmation
|
|
||||||
$0 --dry-run # Preview deployment without changes (RECOMMENDED)
|
|
||||||
$0 --force # Deploy without confirmation (DANGEROUS!)
|
|
||||||
$0 --rollback # Rollback to previous version
|
|
||||||
|
|
||||||
PRODUCTION SAFETY:
|
|
||||||
- Always run --dry-run first
|
|
||||||
- Creates automatic backups before deployment
|
|
||||||
- Health checks ensure successful deployment
|
|
||||||
- Rollback available if deployment fails
|
|
||||||
- Passwordless sudo automatically disabled after deployment
|
|
||||||
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
check_dependencies() {
|
|
||||||
log_step "Checking dependencies for production deployment"
|
|
||||||
|
|
||||||
# Check if source directory exists
|
|
||||||
if [[ ! -d "$SOURCE_DIR" ]]; then
|
|
||||||
log_error "Source directory not found: $SOURCE_DIR"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check SSH connectivity to aitvaras
|
|
||||||
if ! ssh -o ConnectTimeout=10 -o BatchMode=yes "$AITVARAS_HOST" exit 2>/dev/null; then
|
|
||||||
log_error "Cannot connect to aitvaras via SSH"
|
|
||||||
log_info "Please ensure SSH key is set up for $AITVARAS_HOST"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Enable passwordless sudo for deployment
|
|
||||||
if ! enable_passwordless_sudo; then
|
|
||||||
log_error "Cannot enable passwordless sudo on aitvaras"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check rsync availability
|
|
||||||
if ! command -v rsync &> /dev/null; then
|
|
||||||
log_error "rsync is required but not installed"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
log_success "All dependencies OK"
|
|
||||||
}
|
|
||||||
|
|
||||||
get_backup_timestamp() {
|
|
||||||
date +"%Y%m%d_%H%M%S"
|
|
||||||
}
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# SSH REMOTE EXECUTION FUNCTIONS
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
aitvaras_exec() {
|
|
||||||
ssh "$AITVARAS_HOST" "$@"
|
|
||||||
}
|
|
||||||
|
|
||||||
aitvaras_exec_sudo() {
|
|
||||||
ssh "$AITVARAS_HOST" "sudo $@"
|
|
||||||
}
|
|
||||||
|
|
||||||
aitvaras_exec_as_furt() {
|
|
||||||
ssh "$AITVARAS_HOST" "sudo -u $SERVICE_USER $@"
|
|
||||||
}
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# SUDO AUTHENTICATION FUNCTIONS
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
enable_passwordless_sudo() {
|
|
||||||
log_step "Enabling temporary passwordless sudo"
|
|
||||||
|
|
||||||
if [[ "$DRY_RUN" == "true" ]]; then
|
|
||||||
log_info "DRY RUN: Would enable passwordless sudo temporarily"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
log_info "Testing current sudo access..."
|
|
||||||
|
|
||||||
# Test if sudo works without password (already configured)
|
|
||||||
if ssh "$AITVARAS_HOST" "sudo -n true" 2>/dev/null; then
|
|
||||||
log_success "Passwordless sudo already enabled"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Need to enable passwordless sudo temporarily
|
|
||||||
log_info "Setting up temporary passwordless sudo for deployment..."
|
|
||||||
log_warning "⚠️ This will require your password ONCE to enable passwordless mode"
|
|
||||||
|
|
||||||
# Use ssh -t for interactive terminal to set up passwordless mode
|
|
||||||
if ssh -t "$AITVARAS_HOST" "echo 'michael ALL=(ALL) NOPASSWD: ALL' | sudo tee /etc/sudoers.d/99-deployment-passwordless"; then
|
|
||||||
|
|
||||||
# Verify it works
|
|
||||||
if ssh "$AITVARAS_HOST" "sudo -n true" 2>/dev/null; then
|
|
||||||
log_success "Temporary passwordless sudo enabled"
|
|
||||||
return 0
|
|
||||||
else
|
|
||||||
log_error "Failed to verify passwordless sudo"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
log_error "Failed to enable passwordless sudo"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
disable_passwordless_sudo() {
|
|
||||||
if [[ "$DRY_RUN" != "true" ]]; then
|
|
||||||
log_info "Removing temporary passwordless sudo configuration..."
|
|
||||||
ssh "$AITVARAS_HOST" "sudo rm -f /etc/sudoers.d/99-deployment-passwordless" 2>/dev/null || true
|
|
||||||
log_success "Passwordless sudo disabled - security restored"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# USER MANAGEMENT FUNCTIONS
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
create_service_user() {
|
|
||||||
log_step "Creating service user"
|
|
||||||
|
|
||||||
if [[ "$DRY_RUN" == "true" ]]; then
|
|
||||||
log_info "DRY RUN: Would create user $SERVICE_USER with group $SERVICE_GROUP"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check if user already exists
|
|
||||||
if aitvaras_exec "id $SERVICE_USER &>/dev/null"; then
|
|
||||||
log_info "User $SERVICE_USER already exists"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
log_info "Creating system user: $SERVICE_USER"
|
|
||||||
|
|
||||||
# Create system user (Ubuntu style)
|
|
||||||
aitvaras_exec_sudo "useradd --system --shell /usr/sbin/nologin --home-dir $TARGET_DIR --create-home --user-group $SERVICE_USER"
|
|
||||||
|
|
||||||
# Verify user creation
|
|
||||||
if aitvaras_exec "id $SERVICE_USER &>/dev/null"; then
|
|
||||||
log_success "User $SERVICE_USER created successfully"
|
|
||||||
else
|
|
||||||
log_error "Failed to create user $SERVICE_USER"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# DIRECTORY MANAGEMENT FUNCTIONS
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
create_directories() {
|
|
||||||
log_step "Creating directory structure"
|
|
||||||
|
|
||||||
if [[ "$DRY_RUN" == "true" ]]; then
|
|
||||||
log_info "DRY RUN: Would create directories:"
|
|
||||||
local directories=("$TARGET_DIR" "$CONFIG_DIR" "$LOG_DIR" "$RUN_DIR" "$BACKUP_DIR")
|
|
||||||
for dir in "${directories[@]}"; do
|
|
||||||
echo " - $dir"
|
|
||||||
done
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
log_info "Creating directory structure..."
|
|
||||||
|
|
||||||
# Create all directories in one sudo call
|
|
||||||
aitvaras_exec_sudo "mkdir -p $TARGET_DIR $CONFIG_DIR $LOG_DIR $RUN_DIR $BACKUP_DIR"
|
|
||||||
|
|
||||||
# Set ownership and permissions in batch
|
|
||||||
aitvaras_exec_sudo "chown $SERVICE_USER:$SERVICE_GROUP $TARGET_DIR $CONFIG_DIR $LOG_DIR $RUN_DIR $BACKUP_DIR"
|
|
||||||
aitvaras_exec_sudo "chmod 755 $TARGET_DIR $CONFIG_DIR $RUN_DIR $BACKUP_DIR"
|
|
||||||
aitvaras_exec_sudo "chmod 750 $LOG_DIR"
|
|
||||||
|
|
||||||
log_success "Directory structure created"
|
|
||||||
}
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# BACKUP FUNCTIONS
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
create_backup() {
|
|
||||||
local timestamp=$(get_backup_timestamp)
|
|
||||||
local backup_path="$BACKUP_DIR/furt-api_$timestamp"
|
|
||||||
|
|
||||||
log_step "Creating production backup"
|
|
||||||
|
|
||||||
if [[ "$DRY_RUN" == "true" ]]; then
|
|
||||||
log_info "DRY RUN: Would create backup at $backup_path"
|
|
||||||
echo ""
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Create backup directory if it doesn't exist
|
|
||||||
aitvaras_exec_sudo "mkdir -p $BACKUP_DIR"
|
|
||||||
aitvaras_exec_sudo "chown $SERVICE_USER:$SERVICE_GROUP $BACKUP_DIR"
|
|
||||||
|
|
||||||
# Check if target directory exists and has content
|
|
||||||
if aitvaras_exec "test -d $TARGET_DIR && test -n \"\$(ls -A $TARGET_DIR 2>/dev/null)\""; then
|
|
||||||
log_production "Backing up current deployment to: $backup_path"
|
|
||||||
aitvaras_exec_sudo "cp -r $TARGET_DIR $backup_path"
|
|
||||||
aitvaras_exec_sudo "chown -R $SERVICE_USER:$SERVICE_GROUP $backup_path"
|
|
||||||
|
|
||||||
# Set backup metadata
|
|
||||||
aitvaras_exec_sudo "sh -c \"echo 'Backup created: \$(date)' > $backup_path/.backup_info\""
|
|
||||||
aitvaras_exec_sudo "sh -c \"echo 'Original path: $TARGET_DIR' >> $backup_path/.backup_info\""
|
|
||||||
aitvaras_exec_sudo "sh -c \"echo 'Service: $SERVICE_NAME' >> $backup_path/.backup_info\""
|
|
||||||
aitvaras_exec_sudo "sh -c \"echo 'Host: \$(hostname)' >> $backup_path/.backup_info\""
|
|
||||||
aitvaras_exec_sudo "chown $SERVICE_USER:$SERVICE_GROUP $backup_path/.backup_info"
|
|
||||||
|
|
||||||
log_success "Production backup created: $backup_path"
|
|
||||||
echo "$backup_path" # Return backup path
|
|
||||||
else
|
|
||||||
log_warning "No existing deployment found or directory empty, skipping backup"
|
|
||||||
echo "" # Return empty string
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanup_old_backups() {
|
|
||||||
log_step "Cleaning up old backups"
|
|
||||||
|
|
||||||
if [[ "$DRY_RUN" == "true" ]]; then
|
|
||||||
log_info "DRY RUN: Would cleanup old backups (keep last $BACKUP_RETENTION)"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
local backup_count=$(aitvaras_exec "ls -1 $BACKUP_DIR/furt-api_* 2>/dev/null | wc -l" || echo "0")
|
|
||||||
|
|
||||||
if [[ $backup_count -gt $BACKUP_RETENTION ]]; then
|
|
||||||
log_info "Found $backup_count backups, keeping last $BACKUP_RETENTION"
|
|
||||||
aitvaras_exec_sudo "ls -1t $BACKUP_DIR/furt-api_* | tail -n +$((BACKUP_RETENTION + 1)) | xargs rm -rf"
|
|
||||||
log_success "Old backups cleaned up"
|
|
||||||
else
|
|
||||||
log_info "Found $backup_count backups, no cleanup needed"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
list_backups() {
|
|
||||||
log_step "Available production backups"
|
|
||||||
|
|
||||||
if aitvaras_exec "ls -1 $BACKUP_DIR/furt-api_* 2>/dev/null"; then
|
|
||||||
aitvaras_exec "ls -1t $BACKUP_DIR/furt-api_* | head -n 5 | while read backup; do
|
|
||||||
echo \" \$backup\"
|
|
||||||
if [ -f \"\$backup/.backup_info\" ]; then
|
|
||||||
cat \"\$backup/.backup_info\" | sed 's/^/ /'
|
|
||||||
fi
|
|
||||||
echo
|
|
||||||
done"
|
|
||||||
else
|
|
||||||
log_warning "No backups found"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
rollback_deployment() {
|
|
||||||
log_step "Rolling back production deployment"
|
|
||||||
|
|
||||||
# List available backups
|
|
||||||
local latest_backup=$(aitvaras_exec "ls -1t $BACKUP_DIR/furt-api_* 2>/dev/null | head -n 1" || echo "")
|
|
||||||
|
|
||||||
if [[ -z "$latest_backup" ]]; then
|
|
||||||
log_error "No backups available for rollback"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
log_production "Latest backup: $latest_backup"
|
|
||||||
|
|
||||||
if [[ "$FORCE" != "true" ]]; then
|
|
||||||
echo -n "⚠️ PRODUCTION ROLLBACK - Continue? [y/N]: "
|
|
||||||
read -r response
|
|
||||||
if [[ ! "$response" =~ ^[Yy]$ ]]; then
|
|
||||||
log_info "Rollback cancelled"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Stop service
|
|
||||||
stop_service
|
|
||||||
|
|
||||||
# Backup current version (before rollback)
|
|
||||||
local rollback_backup=$(create_backup)
|
|
||||||
if [[ -n "$rollback_backup" ]]; then
|
|
||||||
aitvaras_exec_sudo "mv $rollback_backup ${rollback_backup}_pre_rollback"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Restore from backup
|
|
||||||
aitvaras_exec_sudo "rm -rf $TARGET_DIR"
|
|
||||||
aitvaras_exec_sudo "cp -r $latest_backup $TARGET_DIR"
|
|
||||||
|
|
||||||
# Fix permissions
|
|
||||||
fix_permissions
|
|
||||||
|
|
||||||
# Start service
|
|
||||||
start_service
|
|
||||||
|
|
||||||
# Health check
|
|
||||||
if health_check; then
|
|
||||||
log_success "Production rollback completed successfully"
|
|
||||||
else
|
|
||||||
log_error "Rollback completed but health check failed"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# SERVICE MANAGEMENT FUNCTIONS
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
create_systemd_service() {
|
|
||||||
log_step "Creating systemd service"
|
|
||||||
|
|
||||||
if [[ "$DRY_RUN" == "true" ]]; then
|
|
||||||
log_info "DRY RUN: Would create $SYSTEMD_SERVICE"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
log_info "Creating systemd service: $SERVICE_NAME"
|
|
||||||
|
|
||||||
# Create systemd service file using sudo tee
|
|
||||||
aitvaras_exec_sudo "tee $SYSTEMD_SERVICE > /dev/null << 'EOF'
|
|
||||||
[Unit]
|
|
||||||
Description=Furt API Gateway
|
|
||||||
Documentation=https://gitea.dragons-at-work.de/DAW/furt
|
|
||||||
After=network.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=forking
|
|
||||||
User=$SERVICE_USER
|
|
||||||
Group=$SERVICE_GROUP
|
|
||||||
WorkingDirectory=$TARGET_DIR
|
|
||||||
ExecStart=$TARGET_DIR/scripts/start.sh
|
|
||||||
Restart=always
|
|
||||||
RestartSec=5
|
|
||||||
StandardOutput=journal
|
|
||||||
StandardError=journal
|
|
||||||
|
|
||||||
# Security settings
|
|
||||||
NoNewPrivileges=true
|
|
||||||
PrivateTmp=true
|
|
||||||
ProtectHome=true
|
|
||||||
ProtectSystem=strict
|
|
||||||
ReadWritePaths=$TARGET_DIR $LOG_DIR $RUN_DIR
|
|
||||||
|
|
||||||
# Environment
|
|
||||||
Environment=LUA_COMMAND=/usr/bin/lua5.1
|
|
||||||
Environment=LUA_VERSION=5.1
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
EOF"
|
|
||||||
|
|
||||||
# Reload systemd
|
|
||||||
aitvaras_exec_sudo "systemctl daemon-reload"
|
|
||||||
|
|
||||||
# Enable service
|
|
||||||
aitvaras_exec_sudo "systemctl enable $SERVICE_NAME"
|
|
||||||
|
|
||||||
log_success "Systemd service created and enabled"
|
|
||||||
}
|
|
||||||
|
|
||||||
get_service_status() {
|
|
||||||
if aitvaras_exec "systemctl is-active $SERVICE_NAME >/dev/null 2>&1"; then
|
|
||||||
echo "running"
|
|
||||||
else
|
|
||||||
echo "stopped"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
stop_service() {
|
|
||||||
log_step "Stopping $SERVICE_NAME service"
|
|
||||||
|
|
||||||
if [[ "$DRY_RUN" == "true" ]]; then
|
|
||||||
log_info "DRY RUN: Would stop $SERVICE_NAME service"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
local status=$(get_service_status)
|
|
||||||
|
|
||||||
if [[ "$status" == "running" ]]; then
|
|
||||||
log_production "Stopping production service: $SERVICE_NAME"
|
|
||||||
aitvaras_exec_sudo "systemctl stop $SERVICE_NAME"
|
|
||||||
|
|
||||||
# Wait for service to stop
|
|
||||||
local attempts=0
|
|
||||||
while [[ $attempts -lt 10 ]]; do
|
|
||||||
if [[ $(get_service_status) == "stopped" ]]; then
|
|
||||||
log_success "Service stopped successfully"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
sleep 1
|
|
||||||
((attempts++))
|
|
||||||
done
|
|
||||||
|
|
||||||
log_error "Service did not stop within 10 seconds"
|
|
||||||
return 1
|
|
||||||
else
|
|
||||||
log_info "Service already stopped"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
start_service() {
|
|
||||||
log_step "Starting $SERVICE_NAME service"
|
|
||||||
|
|
||||||
if [[ "$DRY_RUN" == "true" ]]; then
|
|
||||||
log_info "DRY RUN: Would start $SERVICE_NAME service"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
local status=$(get_service_status)
|
|
||||||
if [[ "$status" == "stopped" ]]; then
|
|
||||||
# Check port availability before starting
|
|
||||||
if ! check_port_availability; then
|
|
||||||
log_error "Cannot start service - port 8080 is occupied"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
log_production "Starting production service: $SERVICE_NAME"
|
|
||||||
aitvaras_exec_sudo "systemctl start $SERVICE_NAME"
|
|
||||||
|
|
||||||
# Wait and check if service actually started
|
|
||||||
sleep 3
|
|
||||||
|
|
||||||
local new_status=$(get_service_status)
|
|
||||||
|
|
||||||
if [[ "$new_status" == "running" ]]; then
|
|
||||||
log_success "Service started successfully"
|
|
||||||
else
|
|
||||||
log_error "Failed to start service"
|
|
||||||
# Show service logs for debugging
|
|
||||||
aitvaras_exec_sudo "journalctl -u $SERVICE_NAME --no-pager -n 20" || true
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
log_info "Service already running"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# DEPLOYMENT FUNCTIONS
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
prepare_source() {
|
|
||||||
log_step "Preparing source files"
|
|
||||||
|
|
||||||
# Check source directory structure
|
|
||||||
local required_dirs=("src" "config" "scripts")
|
|
||||||
for dir in "${required_dirs[@]}"; do
|
|
||||||
if [[ ! -d "$SOURCE_DIR/$dir" ]]; then
|
|
||||||
log_error "Required directory missing: $SOURCE_DIR/$dir"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
# Check required files
|
|
||||||
local required_files=("src/main.lua" "scripts/start.sh")
|
|
||||||
for file in "${required_files[@]}"; do
|
|
||||||
if [[ ! -f "$SOURCE_DIR/$file" ]]; then
|
|
||||||
log_error "Required file missing: $SOURCE_DIR/$file"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
log_success "Source files validated"
|
|
||||||
}
|
|
||||||
|
|
||||||
sync_files() {
|
|
||||||
log_step "Syncing files to production"
|
|
||||||
|
|
||||||
# Prepare rsync excludes
|
|
||||||
local excludes=(
|
|
||||||
"--exclude=.env"
|
|
||||||
"--exclude=.env.*"
|
|
||||||
"--exclude=*.backup"
|
|
||||||
"--exclude=*.orig"
|
|
||||||
"--exclude=*.tmp"
|
|
||||||
"--exclude=.git/"
|
|
||||||
"--exclude=.DS_Store"
|
|
||||||
"--exclude=logs/"
|
|
||||||
"--exclude=*.log"
|
|
||||||
"--exclude=tests/"
|
|
||||||
)
|
|
||||||
|
|
||||||
if [[ "$DRY_RUN" == "true" ]]; then
|
|
||||||
log_info "DRY RUN: Would sync the following files:"
|
|
||||||
rsync -avz --dry-run "${excludes[@]}" "$SOURCE_DIR/" "$AITVARAS_HOST:$TARGET_DIR/"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Clean up target directory and recreate with correct permissions
|
|
||||||
log_info "Preparing target directory for sync..."
|
|
||||||
aitvaras_exec_sudo "rm -rf $TARGET_DIR"
|
|
||||||
aitvaras_exec_sudo "mkdir -p $TARGET_DIR"
|
|
||||||
aitvaras_exec_sudo "chown michael:michael $TARGET_DIR"
|
|
||||||
|
|
||||||
# Sync files as michael user (now has permissions)
|
|
||||||
log_production "Syncing files to production..."
|
|
||||||
rsync -avz "${excludes[@]}" \
|
|
||||||
-e "ssh" \
|
|
||||||
"$SOURCE_DIR/" "$AITVARAS_HOST:$TARGET_DIR/"
|
|
||||||
|
|
||||||
# Fix ownership to _furt after successful sync
|
|
||||||
aitvaras_exec_sudo "chown -R $SERVICE_USER:$SERVICE_GROUP $TARGET_DIR"
|
|
||||||
|
|
||||||
log_success "Files synced to production"
|
|
||||||
}
|
|
||||||
|
|
||||||
create_environment_file() {
|
|
||||||
log_step "Creating environment configuration"
|
|
||||||
|
|
||||||
if [[ "$DRY_RUN" == "true" ]]; then
|
|
||||||
if aitvaras_exec "test -f $CONFIG_DIR/environment"; then
|
|
||||||
log_info "DRY RUN: Environment file exists - would NOT overwrite"
|
|
||||||
else
|
|
||||||
log_info "DRY RUN: Would create $CONFIG_DIR/environment"
|
|
||||||
fi
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check if environment file already exists
|
|
||||||
if aitvaras_exec "test -f $CONFIG_DIR/environment"; then
|
|
||||||
log_success "Environment file already exists - preserving existing configuration"
|
|
||||||
log_info "Existing config preserved at: $CONFIG_DIR/environment"
|
|
||||||
|
|
||||||
# Still fix permissions in case they got messed up
|
|
||||||
aitvaras_exec_sudo "chown $SERVICE_USER:$SERVICE_GROUP $CONFIG_DIR/environment"
|
|
||||||
aitvaras_exec_sudo "chmod 640 $CONFIG_DIR/environment"
|
|
||||||
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
log_info "Creating new environment file..."
|
|
||||||
|
|
||||||
# Create environment file for production using sudo tee
|
|
||||||
aitvaras_exec_sudo "tee $CONFIG_DIR/environment > /dev/null << 'EOF'
|
|
||||||
# Furt API Production Environment Configuration
|
|
||||||
# Created by deploy_aitvaras.sh on $(date)
|
|
||||||
|
|
||||||
# Lua Configuration
|
|
||||||
LUA_COMMAND=/usr/bin/lua5.1
|
|
||||||
LUA_VERSION=5.1
|
|
||||||
|
|
||||||
# Server Configuration
|
|
||||||
SERVER_HOST=127.0.0.1
|
|
||||||
SERVER_PORT=8080
|
|
||||||
|
|
||||||
# Logging
|
|
||||||
LOG_LEVEL=info
|
|
||||||
LOG_FILE=$LOG_DIR/furt-api.log
|
|
||||||
|
|
||||||
# SMTP Configuration (to be filled manually)
|
|
||||||
# SMTP_USERNAME=your_smtp_username
|
|
||||||
# SMTP_PASSWORD=your_smtp_password
|
|
||||||
# SMTP_HOST=mail.dragons-at-work.de
|
|
||||||
# SMTP_PORT=465
|
|
||||||
# SMTP_FROM=noreply@dragons-at-work.de
|
|
||||||
# SMTP_TO=michael@dragons-at-work.de
|
|
||||||
|
|
||||||
# Security
|
|
||||||
# Add your production SMTP credentials here manually after deployment
|
|
||||||
EOF"
|
|
||||||
|
|
||||||
# Set proper permissions immediately
|
|
||||||
aitvaras_exec_sudo "chown $SERVICE_USER:$SERVICE_GROUP $CONFIG_DIR/environment"
|
|
||||||
aitvaras_exec_sudo "chmod 640 $CONFIG_DIR/environment"
|
|
||||||
|
|
||||||
log_success "Environment file created with correct permissions"
|
|
||||||
log_warning "⚠️ MANUAL STEP REQUIRED: Add SMTP credentials to $CONFIG_DIR/environment"
|
|
||||||
}
|
|
||||||
|
|
||||||
fix_permissions() {
|
|
||||||
log_step "Fixing permissions"
|
|
||||||
|
|
||||||
if [[ "$DRY_RUN" == "true" ]]; then
|
|
||||||
log_info "DRY RUN: Would set ownership to $SERVICE_USER:$SERVICE_GROUP"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Set ownership in batch
|
|
||||||
aitvaras_exec_sudo "chown -R $SERVICE_USER:$SERVICE_GROUP $TARGET_DIR $CONFIG_DIR $LOG_DIR $RUN_DIR"
|
|
||||||
|
|
||||||
# Set permissions for scripts
|
|
||||||
aitvaras_exec_sudo "chmod +x $TARGET_DIR/scripts/*.sh"
|
|
||||||
|
|
||||||
# Set permissions for config directory
|
|
||||||
aitvaras_exec_sudo "chmod 755 $CONFIG_DIR"
|
|
||||||
|
|
||||||
# Set permissions for environment file only if it exists
|
|
||||||
if aitvaras_exec "test -f $CONFIG_DIR/environment"; then
|
|
||||||
aitvaras_exec_sudo "chmod 640 $CONFIG_DIR/environment"
|
|
||||||
fi
|
|
||||||
|
|
||||||
log_success "Permissions fixed"
|
|
||||||
}
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# HEALTH CHECK FUNCTIONS
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
check_port_availability() {
|
|
||||||
log_step "Checking port availability"
|
|
||||||
|
|
||||||
local port="8080" # furt-api port
|
|
||||||
|
|
||||||
if aitvaras_exec "netstat -an | grep -q ':$port'"; then
|
|
||||||
log_warning "Port $port is already in use"
|
|
||||||
aitvaras_exec "netstat -an | grep ':$port'" || true
|
|
||||||
|
|
||||||
# Try to identify what's using the port
|
|
||||||
log_info "Checking what's using port $port..."
|
|
||||||
aitvaras_exec "sudo ss -tlnp | grep ':$port'" || true
|
|
||||||
|
|
||||||
return 1
|
|
||||||
else
|
|
||||||
log_success "Port $port is available"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
health_check() {
|
|
||||||
log_step "Running production health check"
|
|
||||||
|
|
||||||
local retries=$HEALTH_RETRIES
|
|
||||||
while [[ $retries -gt 0 ]]; do
|
|
||||||
log_info "Health check attempt $((HEALTH_RETRIES - retries + 1))/$HEALTH_RETRIES"
|
|
||||||
|
|
||||||
if aitvaras_exec "curl -s --max-time $HEALTH_TIMEOUT $HEALTH_URL >/dev/null 2>&1"; then
|
|
||||||
log_success "Production health check passed"
|
|
||||||
|
|
||||||
# Get health check response
|
|
||||||
local health_response=$(aitvaras_exec "curl -s --max-time $HEALTH_TIMEOUT $HEALTH_URL" || echo "")
|
|
||||||
if [[ -n "$health_response" ]]; then
|
|
||||||
log_info "Health response: $health_response"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Additional status info
|
|
||||||
local service_status=$(get_service_status)
|
|
||||||
log_info "Service status: $service_status"
|
|
||||||
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
((retries--))
|
|
||||||
if [[ $retries -gt 0 ]]; then
|
|
||||||
log_info "Health check failed, retrying in 5 seconds..."
|
|
||||||
sleep 5
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
log_error "Production health check failed after $HEALTH_RETRIES attempts"
|
|
||||||
log_info "Debugging service status..."
|
|
||||||
local service_status=$(get_service_status)
|
|
||||||
log_info "Service status: $service_status"
|
|
||||||
|
|
||||||
# Show service logs
|
|
||||||
log_info "Recent service logs:"
|
|
||||||
aitvaras_exec_sudo "journalctl -u $SERVICE_NAME --no-pager -n 10" || true
|
|
||||||
|
|
||||||
# Check if port is at least listening
|
|
||||||
if aitvaras_exec "netstat -an | grep -q ':8080.*LISTEN'"; then
|
|
||||||
log_warning "Port 8080 is listening but health check failed - possible service issue"
|
|
||||||
else
|
|
||||||
log_error "Port 8080 is not listening - service not running"
|
|
||||||
fi
|
|
||||||
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# MAIN DEPLOYMENT FUNCTION
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
deploy() {
|
|
||||||
log_step "Starting PRODUCTION deployment: karl → aitvaras"
|
|
||||||
log_production "⚠️ THIS IS A PRODUCTION DEPLOYMENT ⚠️"
|
|
||||||
|
|
||||||
# Pre-deployment checks
|
|
||||||
check_dependencies
|
|
||||||
prepare_source
|
|
||||||
|
|
||||||
# Show deployment summary
|
|
||||||
log_info "Production Deployment Summary:"
|
|
||||||
log_info " Source: $SOURCE_DIR"
|
|
||||||
log_info " Target: $AITVARAS_HOST:$TARGET_DIR"
|
|
||||||
log_info " Service: $SERVICE_NAME (systemd)"
|
|
||||||
log_info " User: $SERVICE_USER"
|
|
||||||
log_info " Dry Run: $DRY_RUN"
|
|
||||||
|
|
||||||
if [[ "$DRY_RUN" == "true" ]]; then
|
|
||||||
log_warning "DRY RUN MODE - No changes will be made"
|
|
||||||
else
|
|
||||||
log_production "🚨 PRODUCTION MODE - Changes will be applied immediately!"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Production confirmation prompt
|
|
||||||
if [[ "$FORCE" != "true" && "$DRY_RUN" != "true" ]]; then
|
|
||||||
echo ""
|
|
||||||
echo -e "${RED}⚠️ PRODUCTION DEPLOYMENT WARNING ⚠️${NC}"
|
|
||||||
echo -e "This will deploy to the live production server aitvaras."
|
|
||||||
echo -e "All changes will immediately affect live services."
|
|
||||||
echo ""
|
|
||||||
echo -n "Continue with PRODUCTION deployment? [y/N]: "
|
|
||||||
read -r response
|
|
||||||
if [[ ! "$response" =~ ^[Yy]$ ]]; then
|
|
||||||
log_info "Production deployment cancelled"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Create service user and directories
|
|
||||||
create_service_user
|
|
||||||
create_directories
|
|
||||||
|
|
||||||
# Create backup before deployment
|
|
||||||
local backup_path=""
|
|
||||||
if [[ "$DRY_RUN" != "true" ]]; then
|
|
||||||
backup_path=$(create_backup)
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Stop service if running
|
|
||||||
if [[ "$DRY_RUN" != "true" ]]; then
|
|
||||||
stop_service
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Deploy files
|
|
||||||
sync_files
|
|
||||||
|
|
||||||
# Create configuration
|
|
||||||
create_environment_file
|
|
||||||
|
|
||||||
# Fix permissions (after environment file exists)
|
|
||||||
fix_permissions
|
|
||||||
|
|
||||||
# Create systemd service
|
|
||||||
create_systemd_service
|
|
||||||
|
|
||||||
# Start service
|
|
||||||
if [[ "$DRY_RUN" != "true" ]]; then
|
|
||||||
# Check if port is available before starting
|
|
||||||
if ! check_port_availability; then
|
|
||||||
log_error "Cannot start service - port conflict detected"
|
|
||||||
log_info "Check what's using port 8080: sudo ss -tlnp | grep :8080"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if start_service; then
|
|
||||||
# Health check
|
|
||||||
if health_check; then
|
|
||||||
log_success "🎉 PRODUCTION DEPLOYMENT COMPLETED SUCCESSFULLY!"
|
|
||||||
log_production "Service is live at: https://api.dragons-at-work.de"
|
|
||||||
|
|
||||||
# Cleanup old backups
|
|
||||||
cleanup_old_backups
|
|
||||||
|
|
||||||
# Disable passwordless sudo (security)
|
|
||||||
disable_passwordless_sudo
|
|
||||||
|
|
||||||
# Show next steps
|
|
||||||
echo ""
|
|
||||||
log_info "📝 MANUAL STEPS REQUIRED:"
|
|
||||||
log_info "1. Add SMTP credentials to: $CONFIG_DIR/environment"
|
|
||||||
log_info "2. Test the API: curl https://api.dragons-at-work.de/health"
|
|
||||||
log_info "3. Monitor logs: sudo journalctl -u $SERVICE_NAME -f"
|
|
||||||
|
|
||||||
else
|
|
||||||
log_error "Production deployment failed health check"
|
|
||||||
|
|
||||||
# Disable passwordless sudo even on failure
|
|
||||||
disable_passwordless_sudo
|
|
||||||
|
|
||||||
# Offer rollback
|
|
||||||
if [[ -n "$backup_path" ]]; then
|
|
||||||
echo -n "🔄 Rollback to previous version? [y/N]: "
|
|
||||||
read -r response
|
|
||||||
if [[ "$response" =~ ^[Yy]$ ]]; then
|
|
||||||
log_production "Rolling back production deployment..."
|
|
||||||
# Need to re-enable passwordless sudo for rollback
|
|
||||||
enable_passwordless_sudo
|
|
||||||
aitvaras_exec_sudo "rm -rf $TARGET_DIR"
|
|
||||||
aitvaras_exec_sudo "cp -r $backup_path $TARGET_DIR"
|
|
||||||
fix_permissions
|
|
||||||
start_service
|
|
||||||
health_check
|
|
||||||
disable_passwordless_sudo
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
log_error "Failed to start service after production deployment"
|
|
||||||
disable_passwordless_sudo
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
log_success "Dry run completed - no changes made to production"
|
|
||||||
echo ""
|
|
||||||
log_info "To execute this deployment for real:"
|
|
||||||
log_info " $0"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# MAIN SCRIPT LOGIC
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
# Parse command line arguments
|
|
||||||
DRY_RUN=false
|
|
||||||
ROLLBACK=false
|
|
||||||
FORCE=false
|
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case $1 in
|
|
||||||
--dry-run)
|
|
||||||
DRY_RUN=true
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
--rollback)
|
|
||||||
ROLLBACK=true
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
--force)
|
|
||||||
FORCE=true
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
--help)
|
|
||||||
usage
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
log_error "Unknown option: $1"
|
|
||||||
usage
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
# Main execution
|
|
||||||
main() {
|
|
||||||
log_production "Furt API Production Deployment Script - karl → aitvaras"
|
|
||||||
log_info "$(date)"
|
|
||||||
|
|
||||||
if [[ "$ROLLBACK" == "true" ]]; then
|
|
||||||
rollback_deployment
|
|
||||||
else
|
|
||||||
deploy
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Trap for cleanup on exit
|
|
||||||
cleanup() {
|
|
||||||
local exit_code=$?
|
|
||||||
|
|
||||||
# Always disable passwordless sudo (security!)
|
|
||||||
disable_passwordless_sudo
|
|
||||||
|
|
||||||
if [[ $exit_code -ne 0 && "$DRY_RUN" != "true" ]]; then
|
|
||||||
log_error "Production deployment failed (exit code: $exit_code)"
|
|
||||||
log_info "Check service status: ssh $AITVARAS_HOST \"sudo systemctl status $SERVICE_NAME\""
|
|
||||||
log_info "Check logs: ssh $AITVARAS_HOST \"sudo journalctl -u $SERVICE_NAME -n 20\""
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
trap cleanup EXIT
|
|
||||||
|
|
||||||
# Run main function
|
|
||||||
main "$@"
|
|
||||||
|
|
||||||
|
|
@ -1,695 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
# scripts/deploy/deploy_walter.sh
|
|
||||||
# Deployment script: karl (development) → walter (OpenBSD staging)
|
|
||||||
#
|
|
||||||
# Usage:
|
|
||||||
# ./scripts/deploy/deploy_walter.sh [--dry-run] [--rollback] [--force]
|
|
||||||
#
|
|
||||||
# Dragons@Work - Furt API-Gateway Deployment
|
|
||||||
# Version: 1.0
|
|
||||||
|
|
||||||
set -euo pipefail # Exit on error, undefined vars, pipe failures
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# CONFIGURATION
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
# Source (karl development)
|
|
||||||
SOURCE_DIR="/home/michael/Develop/DAW/furt/furt-lua"
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
PROJECT_ROOT="$(dirname "$(dirname "$SCRIPT_DIR")")" # scripts/deploy/ -> scripts/ -> furt/
|
|
||||||
|
|
||||||
# Target (walter OpenBSD staging)
|
|
||||||
WALTER_HOST="walter" # Assumes SSH config entry (as michael user)
|
|
||||||
TARGET_DIR="/usr/local/furt/furt-lua"
|
|
||||||
SERVICE_USER="_furt"
|
|
||||||
SERVICE_GROUP="_furt"
|
|
||||||
|
|
||||||
# Backup configuration
|
|
||||||
BACKUP_DIR="/usr/local/furt/backups"
|
|
||||||
BACKUP_RETENTION=3 # Keep last 3 backups
|
|
||||||
|
|
||||||
# Health check configuration
|
|
||||||
HEALTH_URL="http://localhost:8080/health"
|
|
||||||
HEALTH_TIMEOUT=10
|
|
||||||
HEALTH_RETRIES=3
|
|
||||||
|
|
||||||
# Colors for output
|
|
||||||
RED='\033[0;31m'
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
BLUE='\033[0;34m'
|
|
||||||
NC='\033[0m' # No Color
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# LOGGING FUNCTIONS
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
log_info() {
|
|
||||||
echo -e "${BLUE}[INFO]${NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
log_success() {
|
|
||||||
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
log_warning() {
|
|
||||||
echo -e "${YELLOW}[WARNING]${NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
log_error() {
|
|
||||||
echo -e "${RED}[ERROR]${NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
log_step() {
|
|
||||||
echo -e "\n${BLUE}==>${NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# UTILITY FUNCTIONS
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
cat << EOF
|
|
||||||
Usage: $0 [OPTIONS]
|
|
||||||
|
|
||||||
Deployment script for furt-lua: karl (development) → walter (OpenBSD staging)
|
|
||||||
|
|
||||||
OPTIONS:
|
|
||||||
--dry-run Show what would be deployed without making changes
|
|
||||||
--rollback Rollback to previous deployment
|
|
||||||
--force Skip confirmation prompts
|
|
||||||
--help Show this help message
|
|
||||||
|
|
||||||
EXAMPLES:
|
|
||||||
$0 # Normal deployment with confirmation
|
|
||||||
$0 --dry-run # Preview deployment without changes
|
|
||||||
$0 --force # Deploy without confirmation
|
|
||||||
$0 --rollback # Rollback to previous version
|
|
||||||
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
check_dependencies() {
|
|
||||||
log_step "Checking dependencies"
|
|
||||||
|
|
||||||
# Check if source directory exists
|
|
||||||
if [[ ! -d "$SOURCE_DIR" ]]; then
|
|
||||||
log_error "Source directory not found: $SOURCE_DIR"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check SSH connectivity to walter
|
|
||||||
if ! ssh -o ConnectTimeout=5 -o BatchMode=yes "$WALTER_HOST" exit 2>/dev/null; then
|
|
||||||
log_error "Cannot connect to walter via SSH"
|
|
||||||
log_info "Please ensure SSH key is set up for $WALTER_HOST"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check rsync availability
|
|
||||||
if ! command -v rsync &> /dev/null; then
|
|
||||||
log_error "rsync is required but not installed"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
log_success "All dependencies OK"
|
|
||||||
}
|
|
||||||
|
|
||||||
get_backup_timestamp() {
|
|
||||||
date +"%Y%m%d_%H%M%S"
|
|
||||||
}
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# SSH REMOTE EXECUTION FUNCTIONS
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
walter_exec() {
|
|
||||||
ssh "$WALTER_HOST" "$@"
|
|
||||||
}
|
|
||||||
|
|
||||||
walter_exec_as_root() {
|
|
||||||
ssh "$WALTER_HOST" "doas $@"
|
|
||||||
}
|
|
||||||
|
|
||||||
walter_exec_as_furt() {
|
|
||||||
ssh "$WALTER_HOST" "doas -u $SERVICE_USER $@"
|
|
||||||
}
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# BACKUP FUNCTIONS
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
create_backup() {
|
|
||||||
local timestamp=$(get_backup_timestamp)
|
|
||||||
local backup_path="$BACKUP_DIR/furt-lua_$timestamp"
|
|
||||||
|
|
||||||
log_step "Creating backup"
|
|
||||||
|
|
||||||
# Create backup directory if it doesn't exist
|
|
||||||
walter_exec_as_root "mkdir -p $BACKUP_DIR"
|
|
||||||
walter_exec_as_root "chown $SERVICE_USER:$SERVICE_GROUP $BACKUP_DIR"
|
|
||||||
|
|
||||||
# Check if target directory exists
|
|
||||||
if walter_exec "test -d $TARGET_DIR"; then
|
|
||||||
log_info "Backing up current deployment to: $backup_path"
|
|
||||||
walter_exec_as_root "cp -r $TARGET_DIR $backup_path"
|
|
||||||
walter_exec_as_root "chown -R $SERVICE_USER:$SERVICE_GROUP $backup_path"
|
|
||||||
|
|
||||||
# Set backup metadata (fix shell redirect issue)
|
|
||||||
walter_exec_as_root "sh -c \"echo 'Backup created: \$(date)' > $backup_path/.backup_info\""
|
|
||||||
walter_exec_as_root "sh -c \"echo 'Original path: $TARGET_DIR' >> $backup_path/.backup_info\""
|
|
||||||
walter_exec_as_root "chown $SERVICE_USER:$SERVICE_GROUP $backup_path/.backup_info"
|
|
||||||
|
|
||||||
log_success "Backup created: $backup_path"
|
|
||||||
echo "$backup_path" # Return backup path
|
|
||||||
else
|
|
||||||
log_warning "No existing deployment found, skipping backup"
|
|
||||||
echo "" # Return empty string
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanup_old_backups() {
|
|
||||||
log_step "Cleaning up old backups"
|
|
||||||
|
|
||||||
local backup_count=$(walter_exec "ls -1 $BACKUP_DIR/furt-lua_* 2>/dev/null | wc -l" || echo "0")
|
|
||||||
|
|
||||||
if [[ $backup_count -gt $BACKUP_RETENTION ]]; then
|
|
||||||
log_info "Found $backup_count backups, keeping last $BACKUP_RETENTION"
|
|
||||||
walter_exec_as_root "ls -1t $BACKUP_DIR/furt-lua_* | tail -n +$((BACKUP_RETENTION + 1)) | xargs rm -rf"
|
|
||||||
log_success "Old backups cleaned up"
|
|
||||||
else
|
|
||||||
log_info "Found $backup_count backups, no cleanup needed"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
list_backups() {
|
|
||||||
log_step "Available backups"
|
|
||||||
|
|
||||||
if walter_exec "ls -1 $BACKUP_DIR/furt-lua_* 2>/dev/null"; then
|
|
||||||
walter_exec "ls -1t $BACKUP_DIR/furt-lua_* | head -n 5 | while read backup; do
|
|
||||||
echo \" \$backup\"
|
|
||||||
if [ -f \"\$backup/.backup_info\" ]; then
|
|
||||||
cat \"\$backup/.backup_info\" | sed 's/^/ /'
|
|
||||||
fi
|
|
||||||
echo
|
|
||||||
done"
|
|
||||||
else
|
|
||||||
log_warning "No backups found"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
rollback_deployment() {
|
|
||||||
log_step "Rolling back deployment"
|
|
||||||
|
|
||||||
# List available backups
|
|
||||||
local latest_backup=$(walter_exec "ls -1t $BACKUP_DIR/furt-lua_* 2>/dev/null | head -n 1" || echo "")
|
|
||||||
|
|
||||||
if [[ -z "$latest_backup" ]]; then
|
|
||||||
log_error "No backups available for rollback"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
log_info "Latest backup: $latest_backup"
|
|
||||||
|
|
||||||
if [[ "$FORCE" != "true" ]]; then
|
|
||||||
echo -n "Rollback to this version? [y/N]: "
|
|
||||||
read -r response
|
|
||||||
if [[ ! "$response" =~ ^[Yy]$ ]]; then
|
|
||||||
log_info "Rollback cancelled"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Stop service
|
|
||||||
stop_service
|
|
||||||
|
|
||||||
# Backup current version (before rollback)
|
|
||||||
local rollback_backup=$(create_backup)
|
|
||||||
if [[ -n "$rollback_backup" ]]; then
|
|
||||||
walter_exec_as_root "mv $rollback_backup ${rollback_backup}_pre_rollback"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Restore from backup
|
|
||||||
walter_exec_as_root "rm -rf $TARGET_DIR"
|
|
||||||
walter_exec_as_root "cp -r $latest_backup $TARGET_DIR"
|
|
||||||
|
|
||||||
# Fix permissions
|
|
||||||
fix_permissions
|
|
||||||
|
|
||||||
# Start service
|
|
||||||
start_service
|
|
||||||
|
|
||||||
# Health check
|
|
||||||
if health_check; then
|
|
||||||
log_success "Rollback completed successfully"
|
|
||||||
else
|
|
||||||
log_error "Rollback completed but health check failed"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# SERVICE MANAGEMENT FUNCTIONS
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
get_service_status() {
|
|
||||||
# Check if furt process is actually running (regardless of rcctl status)
|
|
||||||
if walter_exec "pgrep -u _furt -f 'src/main.lua' >/dev/null 2>&1"; then
|
|
||||||
echo "running"
|
|
||||||
else
|
|
||||||
echo "stopped"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
get_rcctl_status() {
|
|
||||||
# Check OpenBSD service status specifically
|
|
||||||
if walter_exec "rcctl check furt >/dev/null 2>&1"; then
|
|
||||||
echo "running"
|
|
||||||
else
|
|
||||||
echo "stopped"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
stop_service() {
|
|
||||||
log_step "Stopping furt service"
|
|
||||||
|
|
||||||
local process_status=$(get_service_status)
|
|
||||||
local rcctl_status=$(get_rcctl_status)
|
|
||||||
|
|
||||||
if [[ "$process_status" == "running" ]]; then
|
|
||||||
log_info "Furt process is running (rcctl status: $rcctl_status)"
|
|
||||||
|
|
||||||
# Try rcctl stop first if service is managed by rcctl
|
|
||||||
if [[ "$rcctl_status" == "running" ]]; then
|
|
||||||
log_info "Stopping via rcctl..."
|
|
||||||
walter_exec_as_root "rcctl stop furt" || true
|
|
||||||
sleep 2
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check if still running (manual process or rcctl didn't work)
|
|
||||||
if [[ $(get_service_status) == "running" ]]; then
|
|
||||||
log_info "Process still running, stopping manually..."
|
|
||||||
walter_exec_as_root "pkill -f -U $SERVICE_USER 'lua.*main.lua'" || true
|
|
||||||
sleep 2
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Final check
|
|
||||||
local final_status=$(get_service_status)
|
|
||||||
if [[ "$final_status" == "stopped" ]]; then
|
|
||||||
log_success "Service stopped"
|
|
||||||
else
|
|
||||||
log_error "Could not stop service"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
log_info "Service already stopped"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
start_service() {
|
|
||||||
log_step "Starting furt service"
|
|
||||||
|
|
||||||
local status=$(get_service_status)
|
|
||||||
if [[ "$status" == "stopped" ]]; then
|
|
||||||
# Check port availability before starting
|
|
||||||
if ! check_port_availability; then
|
|
||||||
log_error "Cannot start service - port 8080 is occupied"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
log_info "Starting furt service via rcctl..."
|
|
||||||
walter_exec_as_root "rcctl start furt"
|
|
||||||
|
|
||||||
# Wait and check if service actually started
|
|
||||||
sleep 5
|
|
||||||
|
|
||||||
local process_status=$(get_service_status)
|
|
||||||
local rcctl_status=$(get_rcctl_status)
|
|
||||||
|
|
||||||
if [[ "$process_status" == "running" ]]; then
|
|
||||||
log_success "Service started successfully (rcctl: $rcctl_status, process: $process_status)"
|
|
||||||
elif [[ "$rcctl_status" == "running" ]]; then
|
|
||||||
log_warning "rcctl reports running but process not detected - checking port..."
|
|
||||||
if walter_exec "netstat -an | grep -q ':8080.*LISTEN'"; then
|
|
||||||
log_success "Service appears to be running (port 8080 active)"
|
|
||||||
else
|
|
||||||
log_error "Service failed to start properly"
|
|
||||||
# Show service logs for debugging
|
|
||||||
walter_exec "tail -10 /var/log/daemon" || true
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
log_error "Failed to start service"
|
|
||||||
# Show service logs for debugging
|
|
||||||
walter_exec "tail -10 /var/log/daemon" || true
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
log_info "Service already running"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# DEPLOYMENT FUNCTIONS
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
prepare_source() {
|
|
||||||
log_step "Preparing source files"
|
|
||||||
|
|
||||||
# Check source directory structure
|
|
||||||
local required_dirs=("src" "config" "scripts")
|
|
||||||
for dir in "${required_dirs[@]}"; do
|
|
||||||
if [[ ! -d "$SOURCE_DIR/$dir" ]]; then
|
|
||||||
log_error "Required directory missing: $SOURCE_DIR/$dir"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
# Check required files
|
|
||||||
local required_files=("src/main.lua" "scripts/start.sh")
|
|
||||||
for file in "${required_files[@]}"; do
|
|
||||||
if [[ ! -f "$SOURCE_DIR/$file" ]]; then
|
|
||||||
log_error "Required file missing: $SOURCE_DIR/$file"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
log_success "Source files validated"
|
|
||||||
}
|
|
||||||
|
|
||||||
sync_files() {
|
|
||||||
log_step "Syncing files to walter"
|
|
||||||
|
|
||||||
# Prepare rsync excludes
|
|
||||||
local excludes=(
|
|
||||||
"--exclude=.env"
|
|
||||||
"--exclude=.env.*"
|
|
||||||
"--exclude=*.backup"
|
|
||||||
"--exclude=*.orig"
|
|
||||||
"--exclude=*.tmp"
|
|
||||||
"--exclude=.git/"
|
|
||||||
"--exclude=.DS_Store"
|
|
||||||
"--exclude=logs/"
|
|
||||||
"--exclude=*.log"
|
|
||||||
)
|
|
||||||
|
|
||||||
if [[ "$DRY_RUN" == "true" ]]; then
|
|
||||||
log_info "DRY RUN: Would sync the following files:"
|
|
||||||
rsync -avz --dry-run "${excludes[@]}" "$SOURCE_DIR/" "$WALTER_HOST:$TARGET_DIR/"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Create target directory and set initial ownership
|
|
||||||
walter_exec_as_root "mkdir -p $TARGET_DIR"
|
|
||||||
walter_exec_as_root "chown -R $SERVICE_USER:$SERVICE_GROUP $TARGET_DIR"
|
|
||||||
|
|
||||||
# Sync files using rsync with _furt user
|
|
||||||
log_info "Syncing files..."
|
|
||||||
rsync -avz "${excludes[@]}" \
|
|
||||||
-e "ssh" \
|
|
||||||
--rsync-path="doas -u $SERVICE_USER rsync" \
|
|
||||||
"$SOURCE_DIR/" "$WALTER_HOST:$TARGET_DIR/"
|
|
||||||
|
|
||||||
log_success "Files synced"
|
|
||||||
}
|
|
||||||
|
|
||||||
fix_service_file() {
|
|
||||||
log_step "Updating OpenBSD service file"
|
|
||||||
|
|
||||||
if [[ "$DRY_RUN" == "true" ]]; then
|
|
||||||
log_info "DRY RUN: Would update /etc/rc.d/furt"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
log_info "Creating correct service file..."
|
|
||||||
walter_exec_as_root "sh -c 'cat > /etc/rc.d/furt << \"EOF\"
|
|
||||||
#!/bin/ksh
|
|
||||||
|
|
||||||
daemon=\"$TARGET_DIR/scripts/start.sh\"
|
|
||||||
daemon_user=\"$SERVICE_USER\"
|
|
||||||
daemon_cwd=\"$TARGET_DIR\"
|
|
||||||
daemon_flags=\"start\"
|
|
||||||
|
|
||||||
. /etc/rc.d/rc.subr
|
|
||||||
|
|
||||||
# pexp NACH rc.subr überschreiben (Fix für OpenBSD Issue #77)
|
|
||||||
pexp=\"/usr/local/bin/lua src/main.lua.*\"
|
|
||||||
|
|
||||||
rc_cmd \$1
|
|
||||||
EOF'"
|
|
||||||
|
|
||||||
# Make service file executable
|
|
||||||
walter_exec_as_root "chmod +x /etc/rc.d/furt"
|
|
||||||
|
|
||||||
# Enable service if not already enabled
|
|
||||||
if ! walter_exec "rcctl ls on | grep -q furt"; then
|
|
||||||
log_info "Enabling furt service..."
|
|
||||||
walter_exec_as_root "rcctl enable furt"
|
|
||||||
log_success "Service enabled"
|
|
||||||
else
|
|
||||||
log_info "Service already enabled"
|
|
||||||
fi
|
|
||||||
|
|
||||||
log_success "Service file updated"
|
|
||||||
}
|
|
||||||
|
|
||||||
fix_permissions() {
|
|
||||||
log_step "Fixing permissions"
|
|
||||||
|
|
||||||
if [[ "$DRY_RUN" == "true" ]]; then
|
|
||||||
log_info "DRY RUN: Would set ownership to $SERVICE_USER:$SERVICE_GROUP"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Set ownership (already done in sync_files, but ensure it's correct)
|
|
||||||
walter_exec_as_root "chown -R $SERVICE_USER:$SERVICE_GROUP $TARGET_DIR"
|
|
||||||
|
|
||||||
# Set executable permissions for scripts
|
|
||||||
walter_exec_as_root "chmod +x $TARGET_DIR/scripts/*.sh"
|
|
||||||
|
|
||||||
log_success "Permissions fixed"
|
|
||||||
}
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# HEALTH CHECK FUNCTIONS
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
check_port_availability() {
|
|
||||||
log_step "Checking port availability"
|
|
||||||
|
|
||||||
local port="8080" # Default furt port
|
|
||||||
|
|
||||||
if walter_exec "netstat -an | grep -q ':$port'"; then
|
|
||||||
log_warning "Port $port is already in use"
|
|
||||||
walter_exec "netstat -an | grep ':$port'" || true
|
|
||||||
|
|
||||||
# Try to identify what's using the port
|
|
||||||
log_info "Checking what's using port $port..."
|
|
||||||
walter_exec "fstat 2>/dev/null | grep ':$port'" || walter_exec "netstat -anp 2>/dev/null | grep ':$port'" || true
|
|
||||||
|
|
||||||
return 1
|
|
||||||
else
|
|
||||||
log_success "Port $port is available"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
health_check() {
|
|
||||||
log_step "Running health check"
|
|
||||||
|
|
||||||
local retries=$HEALTH_RETRIES
|
|
||||||
while [[ $retries -gt 0 ]]; do
|
|
||||||
log_info "Health check attempt $((HEALTH_RETRIES - retries + 1))/$HEALTH_RETRIES"
|
|
||||||
|
|
||||||
if walter_exec "curl -s --max-time $HEALTH_TIMEOUT $HEALTH_URL >/dev/null 2>&1"; then
|
|
||||||
log_success "Health check passed"
|
|
||||||
|
|
||||||
# Get health check response
|
|
||||||
local health_response=$(walter_exec "curl -s --max-time $HEALTH_TIMEOUT $HEALTH_URL" || echo "")
|
|
||||||
if [[ -n "$health_response" ]]; then
|
|
||||||
log_info "Health response: $health_response"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Additional status info
|
|
||||||
local process_status=$(get_service_status)
|
|
||||||
local rcctl_status=$(get_rcctl_status)
|
|
||||||
log_info "Service status - Process: $process_status, rcctl: $rcctl_status"
|
|
||||||
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
((retries--))
|
|
||||||
if [[ $retries -gt 0 ]]; then
|
|
||||||
log_info "Health check failed, retrying in 5 seconds..."
|
|
||||||
sleep 5
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
log_error "Health check failed after $HEALTH_RETRIES attempts"
|
|
||||||
log_info "Debugging service status..."
|
|
||||||
local process_status=$(get_service_status)
|
|
||||||
local rcctl_status=$(get_rcctl_status)
|
|
||||||
log_info "Process status: $process_status"
|
|
||||||
log_info "rcctl status: $rcctl_status"
|
|
||||||
|
|
||||||
# Check if port is at least listening
|
|
||||||
if walter_exec "netstat -an | grep -q ':8080.*LISTEN'"; then
|
|
||||||
log_warning "Port 8080 is listening but health check failed - possible service issue"
|
|
||||||
else
|
|
||||||
log_error "Port 8080 is not listening - service not running"
|
|
||||||
fi
|
|
||||||
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# MAIN DEPLOYMENT FUNCTION
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
deploy() {
|
|
||||||
log_step "Starting deployment: karl → walter"
|
|
||||||
|
|
||||||
# Pre-deployment checks
|
|
||||||
check_dependencies
|
|
||||||
prepare_source
|
|
||||||
|
|
||||||
# Show deployment summary
|
|
||||||
log_info "Deployment Summary:"
|
|
||||||
log_info " Source: $SOURCE_DIR"
|
|
||||||
log_info " Target: $WALTER_HOST:$TARGET_DIR"
|
|
||||||
log_info " Service User: $SERVICE_USER"
|
|
||||||
log_info " Dry Run: $DRY_RUN"
|
|
||||||
|
|
||||||
if [[ "$DRY_RUN" == "true" ]]; then
|
|
||||||
log_warning "DRY RUN MODE - No changes will be made"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Confirmation prompt
|
|
||||||
if [[ "$FORCE" != "true" && "$DRY_RUN" != "true" ]]; then
|
|
||||||
echo -n "Continue with deployment? [y/N]: "
|
|
||||||
read -r response
|
|
||||||
if [[ ! "$response" =~ ^[Yy]$ ]]; then
|
|
||||||
log_info "Deployment cancelled"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Create backup before deployment
|
|
||||||
local backup_path=""
|
|
||||||
if [[ "$DRY_RUN" != "true" ]]; then
|
|
||||||
backup_path=$(create_backup)
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Stop service
|
|
||||||
if [[ "$DRY_RUN" != "true" ]]; then
|
|
||||||
stop_service
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Deploy files
|
|
||||||
sync_files
|
|
||||||
fix_permissions
|
|
||||||
|
|
||||||
# Update service file for correct paths
|
|
||||||
fix_service_file
|
|
||||||
|
|
||||||
# Start service
|
|
||||||
if [[ "$DRY_RUN" != "true" ]]; then
|
|
||||||
# Check if port is available before starting
|
|
||||||
if ! check_port_availability; then
|
|
||||||
log_error "Cannot start service - port conflict detected"
|
|
||||||
log_info "Try: ssh walter \"doas pkill -f 'lua.*main.lua'\" to kill conflicting processes"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if start_service; then
|
|
||||||
# Health check
|
|
||||||
if health_check; then
|
|
||||||
log_success "Deployment completed successfully!"
|
|
||||||
|
|
||||||
# Cleanup old backups
|
|
||||||
cleanup_old_backups
|
|
||||||
else
|
|
||||||
log_error "Deployment failed health check"
|
|
||||||
|
|
||||||
# Offer rollback
|
|
||||||
if [[ -n "$backup_path" ]]; then
|
|
||||||
echo -n "Rollback to previous version? [y/N]: "
|
|
||||||
read -r response
|
|
||||||
if [[ "$response" =~ ^[Yy]$ ]]; then
|
|
||||||
log_info "Rolling back..."
|
|
||||||
walter_exec_as_root "rm -rf $TARGET_DIR"
|
|
||||||
walter_exec_as_root "cp -r $backup_path $TARGET_DIR"
|
|
||||||
fix_permissions
|
|
||||||
start_service
|
|
||||||
health_check
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
log_error "Failed to start service after deployment"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
log_success "Dry run completed - no changes made"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# MAIN SCRIPT LOGIC
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
# Parse command line arguments
|
|
||||||
DRY_RUN=false
|
|
||||||
ROLLBACK=false
|
|
||||||
FORCE=false
|
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case $1 in
|
|
||||||
--dry-run)
|
|
||||||
DRY_RUN=true
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
--rollback)
|
|
||||||
ROLLBACK=true
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
--force)
|
|
||||||
FORCE=true
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
--help)
|
|
||||||
usage
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
log_error "Unknown option: $1"
|
|
||||||
usage
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
# Main execution
|
|
||||||
main() {
|
|
||||||
log_info "Furt Deployment Script - karl → walter"
|
|
||||||
log_info "$(date)"
|
|
||||||
|
|
||||||
if [[ "$ROLLBACK" == "true" ]]; then
|
|
||||||
rollback_deployment
|
|
||||||
else
|
|
||||||
deploy
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Trap for cleanup on exit
|
|
||||||
cleanup() {
|
|
||||||
if [[ $? -ne 0 ]]; then
|
|
||||||
log_error "Deployment failed"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
trap cleanup EXIT
|
|
||||||
|
|
||||||
# Run main function
|
|
||||||
main "$@"
|
|
||||||
|
|
||||||
18
scripts/manual_mail_test.sh
Normal file
18
scripts/manual_mail_test.sh
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Manual SMTP test with corrected JSON
|
||||||
|
|
||||||
|
echo "Testing SMTP with corrected JSON..."
|
||||||
|
|
||||||
|
# Simple test without timestamp embedding
|
||||||
|
curl -X POST http://127.0.0.1:8080/v1/mail/send \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"name": "Furt Test User",
|
||||||
|
"email": "michael@dragons-at-work.de",
|
||||||
|
"subject": "Furt SMTP Test Success!",
|
||||||
|
"message": "This is a test email from Furt Lua HTTP-Server. SMTP Integration working!"
|
||||||
|
}'
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Check response above for success:true"
|
||||||
|
|
||||||
80
scripts/production_test_sequence.sh
Normal file
80
scripts/production_test_sequence.sh
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Production Test für api.dragons-at-work.de
|
||||||
|
|
||||||
|
echo "Testing Production API via Apache Proxy"
|
||||||
|
echo "======================================="
|
||||||
|
|
||||||
|
# Test 1: HTTPS Health Check
|
||||||
|
echo ""
|
||||||
|
echo "[1] Testing HTTPS Health Check..."
|
||||||
|
https_health=$(curl -s https://api.dragons-at-work.de/health)
|
||||||
|
echo "HTTPS Response: $https_health"
|
||||||
|
|
||||||
|
if echo "$https_health" | grep -q "healthy"; then
|
||||||
|
echo "[OK] HTTPS Proxy working"
|
||||||
|
else
|
||||||
|
echo "[ERROR] HTTPS Proxy failed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test 2: SMTP Status via HTTPS
|
||||||
|
echo ""
|
||||||
|
echo "[2] Testing SMTP Configuration via HTTPS..."
|
||||||
|
if echo "$https_health" | grep -q '"smtp_configured":true'; then
|
||||||
|
echo "[OK] SMTP configured and accessible via HTTPS"
|
||||||
|
else
|
||||||
|
echo "[ERROR] SMTP not configured or not accessible"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test 3: CORS Headers
|
||||||
|
echo ""
|
||||||
|
echo "[3] Testing CORS Headers..."
|
||||||
|
cors_test=$(curl -s -I https://api.dragons-at-work.de/health | grep -i "access-control")
|
||||||
|
if [ -n "$cors_test" ]; then
|
||||||
|
echo "[OK] CORS headers present: $cors_test"
|
||||||
|
else
|
||||||
|
echo "[WARN] CORS headers missing - add to Apache config"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test 4: Production Mail Test
|
||||||
|
echo ""
|
||||||
|
echo "[4] Testing Production Mail via HTTPS..."
|
||||||
|
echo "WARNING: This sends real email via production API"
|
||||||
|
read -p "Continue with production mail test? (y/N): " -n 1 -r
|
||||||
|
echo
|
||||||
|
|
||||||
|
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||||
|
echo "Sending production test email..."
|
||||||
|
|
||||||
|
prod_mail_response=$(curl -s -X POST https://api.dragons-at-work.de/v1/mail/send \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"name": "Production Test",
|
||||||
|
"email": "test@dragons-at-work.de",
|
||||||
|
"subject": "Production API Test - Apache Proxy Success!",
|
||||||
|
"message": "This email was sent via the production API at api.dragons-at-work.de through Apache proxy to Furt-Lua backend. HTTPS integration working!"
|
||||||
|
}')
|
||||||
|
|
||||||
|
echo "Production Response: $prod_mail_response"
|
||||||
|
|
||||||
|
if echo "$prod_mail_response" | grep -q '"success":true'; then
|
||||||
|
echo "[OK] PRODUCTION MAIL SENT VIA HTTPS!"
|
||||||
|
echo "Check admin@dragons-at-work.de for delivery confirmation"
|
||||||
|
else
|
||||||
|
echo "[ERROR] Production mail failed"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Skipping production mail test"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test 5: Security Headers
|
||||||
|
echo ""
|
||||||
|
echo "[5] Testing Security Headers..."
|
||||||
|
security_headers=$(curl -s -I https://api.dragons-at-work.de/health)
|
||||||
|
echo "Security Headers:"
|
||||||
|
echo "$security_headers" | grep -i "x-content-type-options\|x-frame-options\|strict-transport"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Production Test Complete!"
|
||||||
|
echo "========================"
|
||||||
|
echo "Next: Hugo integration with https://api.dragons-at-work.de/v1/mail/send"
|
||||||
Loading…
Add table
Add a link
Reference in a new issue