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:
@@ -15,6 +15,9 @@ import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"git.netcell-it.de/projekte/edgeguard-native/internal/cluster"
|
||||
"git.netcell-it.de/projekte/edgeguard-native/internal/database"
|
||||
"git.netcell-it.de/projekte/edgeguard-native/internal/license"
|
||||
"git.netcell-it.de/projekte/edgeguard-native/internal/services/acme"
|
||||
@@ -25,7 +28,7 @@ import (
|
||||
"git.netcell-it.de/projekte/edgeguard-native/internal/services/tlscerts"
|
||||
)
|
||||
|
||||
var version = "1.0.70"
|
||||
var version = "1.0.71"
|
||||
|
||||
const (
|
||||
// renewTickInterval — how often we re-evaluate expiring certs.
|
||||
@@ -45,6 +48,11 @@ const (
|
||||
// alignment ist approximativ, weil time.Ticker bei Boot startet).
|
||||
// Retention: 14 erfolgreiche Backups (default in backup.Service).
|
||||
backupTickInterval = 24 * time.Hour
|
||||
|
||||
// configHashTickInterval — alle 5 min config_hash neu berechnen
|
||||
// und in ha_nodes der eigenen Row schreiben. Cluster-UI nutzt
|
||||
// das fürs Drift-Banner — pro-Mutation-Refresh wäre teuer.
|
||||
configHashTickInterval = 5 * time.Minute
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -89,12 +97,22 @@ func main() {
|
||||
}
|
||||
runLicenseVerify(ctx, licClient, licKeyStore, licRepo, nodeID)
|
||||
|
||||
// Lokale Node-ID für config-hash-refresh. EnsureNodeID liefert
|
||||
// dieselbe ID die die API hat (gleiches /var/lib/edgeguard/node-id).
|
||||
localID, _ := cluster.EnsureNodeID("")
|
||||
slog.Info("scheduler: config-hash refresh enabled", "tick", configHashTickInterval, "node_id", localID)
|
||||
// Initial-Refresh damit /cluster/status nach API+Scheduler-Boot
|
||||
// nicht 5min auf den ersten Wert wartet.
|
||||
runConfigHash(ctx, pool, localID)
|
||||
|
||||
renewTick := time.NewTicker(renewTickInterval)
|
||||
defer renewTick.Stop()
|
||||
licTick := time.NewTicker(licenseTickInterval)
|
||||
defer licTick.Stop()
|
||||
backupTick := time.NewTicker(backupTickInterval)
|
||||
defer backupTick.Stop()
|
||||
hashTick := time.NewTicker(configHashTickInterval)
|
||||
defer hashTick.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
@@ -106,10 +124,30 @@ func main() {
|
||||
runLicenseVerify(ctx, licClient, licKeyStore, licRepo, nodeID)
|
||||
case <-backupTick.C:
|
||||
runBackup(ctx, backupSvc, version)
|
||||
case <-hashTick.C:
|
||||
runConfigHash(ctx, pool, localID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// runConfigHash berechnet den Hash und schreibt ihn in ha_nodes.
|
||||
// Pool kann nil sein (scheduler-pool-fail beim boot) — dann no-op.
|
||||
func runConfigHash(ctx context.Context, pool *pgxpoolPool, localID string) {
|
||||
if pool == nil || localID == "" {
|
||||
return
|
||||
}
|
||||
hash, err := cluster.RefreshLocalHash(ctx, pool, localID)
|
||||
if err != nil {
|
||||
slog.Warn("scheduler: config-hash refresh failed", "error", err)
|
||||
return
|
||||
}
|
||||
slog.Debug("scheduler: config-hash refreshed", "hash", hash)
|
||||
}
|
||||
|
||||
// pgxpoolPool ist ein lokaler Alias damit die Signatur stabil bleibt
|
||||
// wenn wir später den pool austauschen wollen (z.B. read-only-replica).
|
||||
type pgxpoolPool = pgxpool.Pool
|
||||
|
||||
// runBackup führt einen scheduled Backup aus + prunet alte. Failures
|
||||
// loggen wir nur — der Tick läuft morgen wieder, kein Notfall.
|
||||
func runBackup(ctx context.Context, svc *backup.Service, version string) {
|
||||
|
||||
Reference in New Issue
Block a user