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!)
|
||||
.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
|
||||
*.luac
|
||||
.luarocks/
|
||||
luarocks.lock
|
||||
|
||||
# Furt-lua runtime/build artifacts
|
||||
furt-lua/bin/
|
||||
furt-lua/logs/
|
||||
furt-lua/tmp/
|
||||
furt-lua/pid/
|
||||
# Furt runtime/build artifacts
|
||||
bin/
|
||||
logs/
|
||||
tmp/
|
||||
pid/
|
||||
|
||||
# Issue creation scripts (these create issues, don't version them)
|
||||
scripts/gitea-issues/
|
||||
|
|
@ -74,8 +53,6 @@ debug.log
|
|||
*.sqlite3
|
||||
|
||||
# Configuration files with secrets
|
||||
config.local.yaml
|
||||
config.production.yaml
|
||||
furt-lua/config/local.lua
|
||||
furt-lua/config/production.lua
|
||||
config.local.lua
|
||||
config.production.lua
|
||||
|
||||
|
|
|
|||
235
README.md
235
README.md
|
|
@ -1,145 +1,160 @@
|
|||
# Furt API Gateway
|
||||
|
||||
**Low-Tech API-Gateway für digitale Souveränität**
|
||||
*Von Go zu C+Lua - Corporate-freie Technologie-Migration*
|
||||
**HTTP-Server in Lua für Service-Integration**
|
||||
|
||||
## Ü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):**
|
||||
- **Von:** Go-basierte Implementation (Corporate-controlled)
|
||||
- **Zu:** C + Lua Implementation (maximale Souveränität)
|
||||
- **Grund:** Elimination von Google-Dependencies für echte digitale Unabhängigkeit
|
||||
- HTTP-Server mit JSON-APIs
|
||||
- Mail-Versendung über SMTP
|
||||
- Request-Routing und Authentication
|
||||
- Health-Check-Endpoints
|
||||
- Konfigurierbare Rate-Limiting
|
||||
- Hugo/Website-Integration
|
||||
|
||||
## Aktuelle Implementierungen
|
||||
## Dependencies
|
||||
|
||||
### 🆕 furt-lua (Aktiv entwickelt)
|
||||
**Pure Lua HTTP-Server - Week 1 ✅**
|
||||
- ✅ HTTP-Server mit lua-socket
|
||||
- ✅ JSON API-Endpoints
|
||||
- ✅ Basic Routing und Error-Handling
|
||||
- ✅ Mail-Service-Grundgerüst
|
||||
- 🔄 SMTP-Integration (Week 2)
|
||||
**Erforderlich:**
|
||||
- `lua` 5.4+
|
||||
- `lua-socket` (HTTP-Server)
|
||||
- `lua-cjson` (JSON-Verarbeitung)
|
||||
|
||||
**Installation:**
|
||||
```bash
|
||||
cd furt-lua/
|
||||
./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)
|
||||
# Arch Linux
|
||||
pacman -S lua lua-socket lua-cjson
|
||||
|
||||
# Start Development-Server
|
||||
cd furt-lua/
|
||||
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"}'
|
||||
# Ubuntu/Debian
|
||||
apt install lua5.4 lua-socket lua-cjson
|
||||
```
|
||||
|
||||
### Testing
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
# Automated Tests
|
||||
cd furt-lua/
|
||||
lua tests/test_http.lua
|
||||
# Repository klonen
|
||||
git clone <repository-url>
|
||||
cd furt
|
||||
|
||||
# Manual curl Tests
|
||||
./scripts/test_curl.sh
|
||||
# Scripts ausführbar machen
|
||||
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) ✅
|
||||
- [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)
|
||||
## API-Endpoints
|
||||
|
||||
### Phase 2: C-Integration (4-6 Wochen)
|
||||
- [ ] C-HTTP-Server für Performance
|
||||
- [ ] C ↔ Lua Bridge
|
||||
- [ ] Memory-Management + Security-Hardening
|
||||
### Health Check
|
||||
```bash
|
||||
GET /health
|
||||
→ {"status":"healthy","service":"furt","version":"1.0.0"}
|
||||
```
|
||||
|
||||
### Phase 3: Infrastructure-Migration (6-12 Monate)
|
||||
- [ ] OpenBSD-Migration
|
||||
- [ ] ISPConfig → eigene Scripts
|
||||
- [ ] Apache → OpenBSD httpd
|
||||
### Mail senden
|
||||
```bash
|
||||
POST /v1/mail/send
|
||||
Content-Type: application/json
|
||||
|
||||
## Dokumentation
|
||||
{
|
||||
"name": "Name",
|
||||
"email": "sender@example.com",
|
||||
"message": "Nachricht"
|
||||
}
|
||||
|
||||
**Development:**
|
||||
- [`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
|
||||
→ {"success":true,"message":"Mail sent"}
|
||||
```
|
||||
|
||||
**API:**
|
||||
- [`furt-lua/README.md`](furt-lua/README.md) - Lua-Implementation Details
|
||||
- `docs/api/` - API-Dokumentation (in Entwicklung)
|
||||
## Konfiguration
|
||||
|
||||
## 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?**
|
||||
- Go = Google-controlled (Module-Proxy, Telemetrie)
|
||||
- Lua = PUC-Rio University (echte Unabhängigkeit)
|
||||
- C + Lua = 50+ Jahre bewährt vs. Corporate-Runtime
|
||||
- Performance: 10x weniger Memory, 5x weniger CPU
|
||||
**Server-Config (config/server.lua):**
|
||||
- Port und Host-Einstellungen
|
||||
- API-Key-Konfiguration
|
||||
- Rate-Limiting-Parameter
|
||||
|
||||
**Teil der Dragons@Work Digital-Sovereignty-Strategie**
|
||||
## Testing
|
||||
|
||||
## Status
|
||||
**Automatische Tests:**
|
||||
```bash
|
||||
lua tests/test_http.lua
|
||||
```
|
||||
|
||||
🚀 **Week 1 Complete:** Lua HTTP-Server funktional
|
||||
🔄 **Week 2 Active:** SMTP-Integration + Hugo-Integration
|
||||
📋 **Week 3+ Planned:** Service-Expansion + C-Migration
|
||||
**Manuelle Tests:**
|
||||
```bash
|
||||
./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
|
||||
|
||||
return {
|
||||
|
|
@ -76,13 +76,13 @@ return {
|
|||
|
||||
-- Mail configuration (for SMTP integration)
|
||||
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,
|
||||
use_ssl = true,
|
||||
username = os.getenv("SMTP_USERNAME"),
|
||||
password = os.getenv("SMTP_PASSWORD"),
|
||||
from_address = os.getenv("SMTP_FROM") or "noreply@dragons-at-work.de",
|
||||
to_address = os.getenv("SMTP_TO") or "michael@dragons-at-work.de"
|
||||
from_address = os.getenv("SMTP_FROM") or "noreply@example.org",
|
||||
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