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:
michael 2025-08-14 09:36:55 +02:00
parent 87c935379b
commit be3b9614d0
38 changed files with 280 additions and 5892 deletions

37
.gitignore vendored
View file

@ -1,37 +1,16 @@
# Environment variables (NEVER commit!) # Environment variables (NEVER commit!)
.env .env
# Go build artifacts
*.exe
*.exe~
*.dll
*.so
*.dylib
furt-gateway
formular2mail-service
sagjan-service
/build/
/dist/
# Go test files
*.test
*.out
coverage.txt
coverage.html
# Go modules
/vendor/
# Lua specific # Lua specific
*.luac *.luac
.luarocks/ .luarocks/
luarocks.lock luarocks.lock
# Furt-lua runtime/build artifacts # Furt runtime/build artifacts
furt-lua/bin/ bin/
furt-lua/logs/ logs/
furt-lua/tmp/ tmp/
furt-lua/pid/ pid/
# Issue creation scripts (these create issues, don't version them) # Issue creation scripts (these create issues, don't version them)
scripts/gitea-issues/ scripts/gitea-issues/
@ -74,8 +53,6 @@ debug.log
*.sqlite3 *.sqlite3
# Configuration files with secrets # Configuration files with secrets
config.local.yaml config.local.lua
config.production.yaml config.production.lua
furt-lua/config/local.lua
furt-lua/config/production.lua

235
README.md
View file

@ -1,145 +1,160 @@
# Furt API Gateway # Furt API Gateway
**Low-Tech API-Gateway für digitale Souveränität** **HTTP-Server in Lua für Service-Integration**
*Von Go zu C+Lua - Corporate-freie Technologie-Migration*
## Überblick ## Überblick
Furt ist ein minimalistischer API-Gateway, der verschiedene Services unter einer einheitlichen API vereint. Der Name "Furt" (germanisch für "Durchgang durch Wasser") symbolisiert die Gateway-Funktion: Alle Requests durchqueren die API-Furt um zu den dahinterliegenden Services zu gelangen. Furt ist ein HTTP-Server der verschiedene Services unter einer API vereint. Aktuell unterstützt es Mail-Versendung über SMTP und bietet eine einfache JSON-API für Web-Integration.
## Technologie-Migration ## Features
🔄 **Strategische Neuausrichtung (Juni 2025):** - HTTP-Server mit JSON-APIs
- **Von:** Go-basierte Implementation (Corporate-controlled) - Mail-Versendung über SMTP
- **Zu:** C + Lua Implementation (maximale Souveränität) - Request-Routing und Authentication
- **Grund:** Elimination von Google-Dependencies für echte digitale Unabhängigkeit - Health-Check-Endpoints
- Konfigurierbare Rate-Limiting
- Hugo/Website-Integration
## Aktuelle Implementierungen ## Dependencies
### 🆕 furt-lua (Aktiv entwickelt) **Erforderlich:**
**Pure Lua HTTP-Server - Week 1 ✅** - `lua` 5.4+
- ✅ HTTP-Server mit lua-socket - `lua-socket` (HTTP-Server)
- ✅ JSON API-Endpoints - `lua-cjson` (JSON-Verarbeitung)
- ✅ Basic Routing und Error-Handling
- ✅ Mail-Service-Grundgerüst
- 🔄 SMTP-Integration (Week 2)
**Installation:**
```bash ```bash
cd furt-lua/ # Arch Linux
./scripts/start.sh
# Server: http://127.0.0.1:8080
```
### 📦 Go-Implementation (Parallel/Legacy)
- Ursprüngliche Planung in `cmd/`, `internal/`
- Wird durch Lua-Version ersetzt
- Referenz für API-Kompatibilität
## Philosophie
- **Technologie-Souveränität**: Nur akademische/unabhängige Technologien
- **Low-Tech-Ansatz**: C + Lua statt Corporate-Runtimes
- **Minimale Dependencies**: < 5 externe Libraries
- **Modulare Architektur**: < 200 Zeilen pro Modul
- **Vollständige Transparenz**: Jede Zeile Code verstehbar
- **Langfristige Stabilität**: 50+ Jahre bewährte Technologien
## Tech-Stack (Final)
**Souveräne Technologien:**
- **C** (GCC + musl) - Kern-Performance
- **Lua** (PUC-Rio University) - Business-Logic
- **LMDB** (Howard Chu/Symas) - Datenbank
- **OpenBSD httpd** - Reverse-Proxy (langfristig)
**Corporate-frei:** Keine Google-, Microsoft-, oder VC-kontrollierten Dependencies
## Services
- **formular2mail**: Kontaktformulare zu E-Mail (Week 1 ✅)
- **sagjan**: Selbst-gehostetes Kommentarsystem
- **lengan**: Projektverwaltung
- **budlam**: Kontaktverwaltung
- **Weitere**: Shop, Newsletter, Kalendar, etc.
## Installation & Entwicklung
### Quick Start (furt-lua)
```bash
# Dependencies (Arch Linux)
pacman -S lua lua-socket lua-cjson pacman -S lua lua-socket lua-cjson
# Start Development-Server # Ubuntu/Debian
cd furt-lua/ apt install lua5.4 lua-socket lua-cjson
chmod +x scripts/start.sh
./scripts/start.sh
# Test
curl -X POST http://127.0.0.1:8080/test \
-H "Content-Type: application/json" \
-d '{"test":"data"}'
``` ```
### Testing ## Installation
```bash ```bash
# Automated Tests # Repository klonen
cd furt-lua/ git clone <repository-url>
lua tests/test_http.lua cd furt
# Manual curl Tests # Scripts ausführbar machen
./scripts/test_curl.sh chmod +x scripts/*.sh
# Server starten
./scripts/start.sh
``` ```
## Roadmap **Server läuft auf:** http://127.0.0.1:8080
### Phase 1: Lua-Foundation (4 Wochen) ✅ ## API-Endpoints
- [x] Week 1: HTTP-Server + Mail-Service-Grundgerüst
- [ ] Week 2: SMTP-Integration + API-Key-Auth
- [ ] Week 3: Service-Expansion (Comments)
- [ ] Week 4: Production-Ready (HTTPS, Systemd)
### Phase 2: C-Integration (4-6 Wochen) ### Health Check
- [ ] C-HTTP-Server für Performance ```bash
- [ ] C ↔ Lua Bridge GET /health
- [ ] Memory-Management + Security-Hardening → {"status":"healthy","service":"furt","version":"1.0.0"}
```
### Phase 3: Infrastructure-Migration (6-12 Monate) ### Mail senden
- [ ] OpenBSD-Migration ```bash
- [ ] ISPConfig → eigene Scripts POST /v1/mail/send
- [ ] Apache → OpenBSD httpd Content-Type: application/json
## Dokumentation {
"name": "Name",
"email": "sender@example.com",
"message": "Nachricht"
}
**Development:** → {"success":true,"message":"Mail sent"}
- [`devdocs/furt_konzept.md`](devdocs/furt_konzept.md) - Technische Architektur ```
- [`devdocs/furt_master_strategy.md`](devdocs/furt_master_strategy.md) - 18-24 Monate Roadmap
- [`devdocs/furt_development_process.md`](devdocs/furt_development_process.md) - Development-Guidelines
**API:** ## Konfiguration
- [`furt-lua/README.md`](furt-lua/README.md) - Lua-Implementation Details
- `docs/api/` - API-Dokumentation (in Entwicklung)
## Technologie-Rationale **Environment Variables (.env):**
```bash
FURT_MAIL_HOST=mail.example.com
FURT_MAIL_PORT=587
FURT_MAIL_USERNAME=user@example.com
FURT_MAIL_PASSWORD=password
FURT_MAIL_TO=empfaenger@example.com
```
**Warum Lua statt Go?** **Server-Config (config/server.lua):**
- Go = Google-controlled (Module-Proxy, Telemetrie) - Port und Host-Einstellungen
- Lua = PUC-Rio University (echte Unabhängigkeit) - API-Key-Konfiguration
- C + Lua = 50+ Jahre bewährt vs. Corporate-Runtime - Rate-Limiting-Parameter
- Performance: 10x weniger Memory, 5x weniger CPU
**Teil der Dragons@Work Digital-Sovereignty-Strategie** ## Testing
## Status **Automatische Tests:**
```bash
lua tests/test_http.lua
```
🚀 **Week 1 Complete:** Lua HTTP-Server funktional **Manuelle Tests:**
🔄 **Week 2 Active:** SMTP-Integration + Hugo-Integration ```bash
📋 **Week 3+ Planned:** Service-Expansion + C-Migration ./scripts/test_curl.sh
## Lizenz # Oder direkt:
curl -X POST http://127.0.0.1:8080/v1/mail/send \
-H "Content-Type: application/json" \
-d '{"name":"Test","email":"test@example.com","message":"Test"}'
```
Apache License 2.0 - Siehe [LICENSE](LICENSE) für Details. ## Deployment
--- **OpenBSD:**
- rc.d-Script in `deployment/openbsd/`
- Systemd-Integration über Scripts
*Furt steht im Einklang mit den Prinzipien digitaler Souveränität und dem Low-Tech-Ansatz des Dragons@Work-Projekts.* **Production-Setup:**
```bash
# Environment-Config kopieren
cp .env.example .env.production
# → SMTP-Credentials anpassen
# Production-Mode starten
export FURT_ENV=production
./scripts/start.sh
```
## Projektstruktur
```
furt/
├── src/ # Lua-Source-Code
│ ├── main.lua # HTTP-Server
│ ├── routes/ # API-Endpoints
│ └── smtp.lua # Mail-Integration
├── config/ # Konfiguration
├── scripts/ # Start/Test-Scripts
├── tests/ # Test-Suite
└── deployment/ # System-Integration
```
## Hugo-Integration
**Shortcode-Beispiel:**
```html
<form action="http://your-server:8080/v1/mail/send" method="POST">
<input name="name" type="text" required>
<input name="email" type="email" required>
<textarea name="message" required></textarea>
<button type="submit">Senden</button>
</form>
```
## Development
**Code-Struktur:**
- Module unter 200 Zeilen
- Funktionen unter 50 Zeilen
- Klare Fehlerbehandlung
- Testbare Komponenten
**Dependencies minimal halten:**
- Nur lua-socket und lua-cjson
- Keine externen HTTP-Libraries
- Standard-Lua-Funktionen bevorzugen

View file

@ -1,4 +1,4 @@
-- furt-lua/config/server.lua -- config/server.lua
-- Server configuration for Furt Lua HTTP-Server -- Server configuration for Furt Lua HTTP-Server
return { return {
@ -76,13 +76,13 @@ return {
-- Mail configuration (for SMTP integration) -- Mail configuration (for SMTP integration)
mail = { mail = {
smtp_server = os.getenv("SMTP_HOST") or "mail.dragons-at-work.de", smtp_server = os.getenv("SMTP_HOST") or "mail.example.org",
smtp_port = tonumber(os.getenv("SMTP_PORT")) or 465, smtp_port = tonumber(os.getenv("SMTP_PORT")) or 465,
use_ssl = true, use_ssl = true,
username = os.getenv("SMTP_USERNAME"), username = os.getenv("SMTP_USERNAME"),
password = os.getenv("SMTP_PASSWORD"), password = os.getenv("SMTP_PASSWORD"),
from_address = os.getenv("SMTP_FROM") or "noreply@dragons-at-work.de", from_address = os.getenv("SMTP_FROM") or "noreply@example.org",
to_address = os.getenv("SMTP_TO") or "michael@dragons-at-work.de" to_address = os.getenv("SMTP_TO") or "admin@example.org"
} }
} }

View file

@ -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

View file

@ -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.

View file

@ -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.**

View file

@ -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.**

View file

@ -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
View file

@ -1,3 +0,0 @@
module furt
go 1.24.3

38
projct-tree.txt Normal file
View 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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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 "$@"

View file

@ -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 "$@"

View 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"

View 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"