package main import ( "context" "database/sql" "fmt" "log" "strings" "time" "github.com/google/uuid" "certigo-addon-backend/providers" ) // StartRenewalScheduler startet den Scheduler für automatische Zertifikatserneuerungen // Der Scheduler prüft regelmäßig die renewal_queue auf fällige Erneuerungen func StartRenewalScheduler() { go func() { ticker := time.NewTicker(5 * time.Minute) // Prüfe alle 5 Minuten defer ticker.Stop() log.Println("Renewal Scheduler gestartet - prüfe alle 5 Minuten auf fällige Erneuerungen") // Führe sofort eine Prüfung durch beim Start processRenewalQueue() // Dann in regelmäßigen Intervallen for range ticker.C { processRenewalQueue() } }() } // processRenewalQueue prüft die renewal_queue auf fällige Erneuerungen und führt sie aus func processRenewalQueue() { // Verwende kürzeren Context für die Queue-Abfrage, um Datenbankblockierungen zu vermeiden queryCtx, queryCancel := context.WithTimeout(context.Background(), 5*time.Second) defer queryCancel() now := time.Now().UTC().Format("2006-01-02 15:04:05") // Hole alle fälligen Queue-Einträge (scheduled_at <= now und status = 'pending') rows, err := db.QueryContext(queryCtx, ` SELECT id, certificate_id, fqdn_id, space_id, scheduled_at FROM renewal_queue WHERE status = 'pending' AND scheduled_at <= ? ORDER BY scheduled_at ASC LIMIT 10 `, now) if err != nil { log.Printf("Fehler beim Abfragen der renewal_queue: %v", err) return } defer rows.Close() var processedCount int for rows.Next() { var queueID, certID, fqdnID, spaceID, scheduledAt string if err := rows.Scan(&queueID, &certID, &fqdnID, &spaceID, &scheduledAt); err != nil { log.Printf("Fehler beim Scannen der Queue-Zeile: %v", err) continue } // Markiere als in Bearbeitung (mit angemessenem Timeout) // Verwende Retry-Logik für Datenbankoperationen, um Blockierungen zu handhaben var updateErr error for retry := 0; retry < 3; retry++ { updateCtx, updateCancel := context.WithTimeout(context.Background(), 10*time.Second) _, updateErr = db.ExecContext(updateCtx, ` UPDATE renewal_queue SET status = 'processing', processed_at = ? WHERE id = ? `, time.Now().UTC().Format("2006-01-02 15:04:05"), queueID) updateCancel() if updateErr == nil { break } if retry < 2 { log.Printf("Warnung: Fehler beim Aktualisieren des Queue-Status (Versuch %d/3), versuche erneut: %v", retry+1, updateErr) time.Sleep(time.Second * time.Duration(retry+1)) // Exponential backoff } } if updateErr != nil { log.Printf("Fehler beim Aktualisieren des Queue-Status nach 3 Versuchen: %v", updateErr) continue } // Führe Erneuerung durch (in separater Goroutine, um Datenbankblockierungen zu vermeiden) go func(qID, cID, fID, sID string) { if err := processCertificateRenewal(cID, fID, sID, qID); err != nil { log.Printf("Fehler bei der Zertifikatserneuerung (Queue ID: %s): %v", qID, err) // Markiere als fehlgeschlagen (mit Retry-Logik) var updateErr error for retry := 0; retry < 3; retry++ { errorCtx, errorCancel := context.WithTimeout(context.Background(), 10*time.Second) _, updateErr = db.ExecContext(errorCtx, ` UPDATE renewal_queue SET status = 'failed', error_message = ? WHERE id = ? `, err.Error(), qID) errorCancel() if updateErr == nil { break } if retry < 2 { time.Sleep(time.Second * time.Duration(retry+1)) } } if updateErr != nil { log.Printf("Fehler beim Aktualisieren des Fehlerstatus nach 3 Versuchen: %v", updateErr) } return } // Markiere als erfolgreich (mit Retry-Logik) var successErr error for retry := 0; retry < 3; retry++ { successCtx, successCancel := context.WithTimeout(context.Background(), 10*time.Second) _, successErr = db.ExecContext(successCtx, ` UPDATE renewal_queue SET status = 'completed' WHERE id = ? `, qID) successCancel() if successErr == nil { break } if retry < 2 { time.Sleep(time.Second * time.Duration(retry+1)) } } if successErr != nil { log.Printf("Fehler beim Markieren als erfolgreich nach 3 Versuchen: %v", successErr) } else { log.Printf("Zertifikatserneuerung erfolgreich verarbeitet (Queue ID: %s, Cert ID: %s, FQDN: %s)", qID, cID, fID) } }(queueID, certID, fqdnID, spaceID) processedCount++ } if processedCount > 0 { log.Printf("Renewal Queue: %d Erneuerungen gestartet", processedCount) } } // processCertificateRenewal führt die tatsächliche Zertifikatserneuerung durch func processCertificateRenewal(certID, fqdnID, spaceID, queueID string) error { // Lade FQDN-Daten (mit angemessenem Timeout) var fqdn FQDN var acmeProviderID, acmeUsername, acmePassword, acmeEmail, acmeKeyID sql.NullString var renewalEnabled sql.NullInt64 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() err := db.QueryRowContext(ctx, ` SELECT id, space_id, fqdn, acme_provider_id, acme_username, acme_password, acme_email, acme_key_id, COALESCE(renewal_enabled, 1) FROM fqdns WHERE id = ? AND space_id = ? `, fqdnID, spaceID).Scan( &fqdn.ID, &fqdn.SpaceID, &fqdn.FQDN, &acmeProviderID, &acmeUsername, &acmePassword, &acmeEmail, &acmeKeyID, &renewalEnabled, ) if err != nil { return err } if acmeProviderID.String != "certigo-acmeproxy" || !acmeUsername.Valid || !acmePassword.Valid || !acmeEmail.Valid { return fmt.Errorf("fqdn hat keine gültigen ACME-Daten") } fqdn.AcmeProviderID = acmeProviderID.String fqdn.AcmeUsername = acmeUsername.String fqdn.AcmePassword = acmePassword.String fqdn.AcmeEmail = acmeEmail.String if acmeKeyID.Valid { fqdn.AcmeKeyID = acmeKeyID.String } // Lade Provider-Konfiguration pm := providers.GetManager() provider, exists := pm.GetProvider("certigo-acmeproxy") if !exists || provider == nil { return fmt.Errorf("acme provider nicht gefunden") } config, err := pm.GetProviderConfig("certigo-acmeproxy") if err != nil { return fmt.Errorf("Fehler beim Laden der Provider-Konfiguration: %v", err) } acmeProxyProvider, ok := provider.(*providers.CertigoACMEProxyProvider) if !ok { return fmt.Errorf("Ungültiger Provider-Typ") } // Erstelle Update- und Cleanup-Funktionen (mit Retry-Logik) updateTokenFunc := func(token string) error { var updateErr error for retry := 0; retry < 3; retry++ { updateCtx, updateCancel := context.WithTimeout(context.Background(), 10*time.Second) _, updateErr = db.ExecContext(updateCtx, "UPDATE fqdns SET acme_challenge_token = ? WHERE id = ?", token, fqdnID) updateCancel() if updateErr == nil { break } if retry < 2 { time.Sleep(time.Second * time.Duration(retry+1)) } } if updateErr != nil { return fmt.Errorf("fehler beim Speichern des Challenge-Tokens: %v", updateErr) } return acmeProxyProvider.UpdateChallengeToken(fqdn.AcmeUsername, fqdn.AcmePassword, token, config.Settings) } cleanupTokenFunc := func() error { var cleanupErr error for retry := 0; retry < 3; retry++ { cleanupCtx, cleanupCancel := context.WithTimeout(context.Background(), 10*time.Second) _, cleanupErr = db.ExecContext(cleanupCtx, "UPDATE fqdns SET acme_challenge_token = NULL WHERE id = ?", fqdnID) cleanupCancel() if cleanupErr == nil { break } if retry < 2 { time.Sleep(time.Second * time.Duration(retry+1)) } } return cleanupErr } // Status-Callback (optional, für Logging) statusCallback := func(status string) { log.Printf("[Renewal Queue %s] Status: %s", queueID, status) } // Generiere TraceID traceID := generateTraceID() log.Printf("Starte automatische Zertifikatserneuerung (Queue ID: %s, TraceID: %s, FQDN: %s)", queueID, traceID, fqdn.FQDN) // Beantrage neues Zertifikat baseFqdn := strings.TrimPrefix(fqdn.FQDN, "*.") result, err := RequestCertificate(baseFqdn, fqdn.AcmeEmail, fqdnID, fqdn.AcmeKeyID, traceID, updateTokenFunc, cleanupTokenFunc, statusCallback) if err != nil { return fmt.Errorf("fehler beim Beantragen des neuen Zertifikats: %v", err) } // Speichere neues Zertifikat if result.Certificate != "" && result.PrivateKey != "" { newCertID := uuid.New().String() certificateID := result.OrderURL if certificateID == "" { certificateID = newCertID } createdAt := time.Now().UTC().Format("2006-01-02 15:04:05") expiresAt, isIntermediate, parseErr := ParseCertificate(result.Certificate) var expiresAtStr string var isIntermediateInt int if parseErr == nil { expiresAtStr = expiresAt.UTC().Format("2006-01-02 15:04:05") if isIntermediate { isIntermediateInt = 1 } } // Verwende separaten Context für INSERT mit Retry-Logik var insertErr error for retry := 0; retry < 3; retry++ { insertCtx, insertCancel := context.WithTimeout(context.Background(), 10*time.Second) _, insertErr = db.ExecContext(insertCtx, ` INSERT INTO certificates (id, fqdn_id, space_id, csr_id, certificate_id, provider_id, certificate_pem, private_key_pem, status, expires_at, is_intermediate, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `, newCertID, fqdnID, spaceID, nil, certificateID, "certigo-acmeproxy", result.Certificate, result.PrivateKey, "issued", expiresAtStr, isIntermediateInt, createdAt) insertCancel() if insertErr == nil { break } if retry < 2 { time.Sleep(time.Second * time.Duration(retry+1)) } } if insertErr != nil { return fmt.Errorf("fehler beim Speichern des neuen Zertifikats: %v", insertErr) } // Markiere altes Zertifikat (optional, falls gewünscht) // Hier könnten wir z.B. den Status auf 'replaced' setzen // Verarbeite RenewalInfo für das neue Zertifikat (falls aktiviert) renewalEnabledValue := true if renewalEnabled.Valid { renewalEnabledValue = renewalEnabled.Int64 == 1 } if renewalEnabledValue { go func() { if err := ProcessRenewalInfoForCertificate(result.Certificate, newCertID, fqdnID, spaceID, true); err != nil { log.Printf("Fehler beim Verarbeiten der RenewalInfo für erneuertes Zertifikat (wird ignoriert): %v", err) } }() } log.Printf("Automatische Zertifikatserneuerung erfolgreich abgeschlossen (Queue ID: %s, Neues Cert ID: %s)", queueID, newCertID) } // Update KeyID falls geändert (mit Retry-Logik) if result.KeyID != "" && result.KeyID != fqdn.AcmeKeyID { var keyIDErr error for retry := 0; retry < 3; retry++ { keyIDCtx, keyIDCancel := context.WithTimeout(context.Background(), 10*time.Second) _, keyIDErr = db.ExecContext(keyIDCtx, "UPDATE fqdns SET acme_key_id = ? WHERE id = ?", result.KeyID, fqdnID) keyIDCancel() if keyIDErr == nil { break } if retry < 2 { time.Sleep(time.Second * time.Duration(retry+1)) } } if keyIDErr != nil { log.Printf("Warnung: Fehler beim Speichern der neuen KeyID nach 3 Versuchen: %v", keyIDErr) } } return nil }