feat(cluster): Config-Hash-Compute für Drift-Detection

Setzt die Foundation aus 1.0.70 fort — bisher war ha_nodes.config_hash
noch NULL und das UI konnte keinen Drift erkennen.

internal/cluster/confighash.go:
  - ComputeConfigHash() berechnet SHA-256 (truncated auf 16 hex chars)
    über alle replizierbaren Tabellen. Pattern 1:1 aus mail-gateway/
    internal/handlers/cluster_status.go (driftHashSpec).
  - Pro Tabelle: md5((to_jsonb(t) - id - updated_at - created_at -
    excludes)::text) per row, dann string_agg ORDER BY rh.
  - Singleton-Tabellen (dns_settings, ntp_settings, mail_config-Stil)
    hashen direkt ohne agg.
  - 23 Tabellen: domains, backends, backend_servers, routing_rules,
    network_interfaces, ip_addresses, tls_certs (mit ExtraExclude
    last_renewed_at + last_error damit cert-renewal keinen drift
    erzeugt), firewall_zones+address_objects+address_groups+services+
    service_groups+rules+nat_rules, wireguard_interfaces+peers,
    forward_proxy_acls, dns_zones+records+settings, ntp_pools+settings,
    static_routes.
  - RefreshLocalHash() schreibt den Hash in die eigene ha_nodes-Row.

Scheduler:
  - 5-min-Tick ruft RefreshLocalHash. Pro-Mutation-Refresh wäre zu
    teuer (jede UI-Action triggert sonst 23 jsonb-Queries).
  - Initial-Refresh beim Scheduler-Boot damit /cluster/status nicht
    5 min auf den ersten Wert wartet.

handlers/cluster.go:
  - Status() ruft RefreshLocalHash mit 2s-Timeout on-demand. Damit
    sieht das UI auch zwischen den Scheduler-Ticks immer frische
    Werte; bei Timeout fallback auf den DB-Wert (eventuell stale).

Verifiziert auf 1.0.71: ha_nodes-Row hat config_hash=728834dce5ca4e48,
scheduler-log "config-hash refresh enabled tick=5m0s".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Debian
2026-05-13 08:33:42 +02:00
parent ea7c356455
commit e07b484a48
7 changed files with 194 additions and 5 deletions

View File

@@ -1,6 +1,8 @@
package handlers
import (
"context"
"log/slog"
"time"
"github.com/gin-gonic/gin"
@@ -50,7 +52,19 @@ type ClusterStatus struct {
}
// Status splittet alle Nodes in local + peers, berechnet mode + health.
// On-demand: bevor wir die Rows lesen refreshen wir den eigenen
// config_hash, sodass das UI immer aktuelle Werte sieht — auch wenn
// der 5min-Scheduler-Tick gerade vorher nicht gelaufen ist.
func (h *ClusterHandler) Status(c *gin.Context) {
if h.Store != nil && h.Store.Pool != nil && h.LocalID != "" {
// 2s Timeout — der Hash-Compute braucht im normal-case <50ms.
// Bei timeout fallen wir auf den (eventuell stale) DB-Wert zurück.
ctx, cancel := context.WithTimeout(c.Request.Context(), 2*time.Second)
if _, err := cluster.RefreshLocalHash(ctx, h.Store.Pool, h.LocalID); err != nil {
slog.Warn("cluster: config_hash refresh failed", "error", err)
}
cancel()
}
all, err := h.Store.List(c.Request.Context())
if err != nil {
response.Internal(c, err)