Files
edgeguard-native/cmd/edgeguard-ctl/cluster_join.go
noroot 808f6fc055 feat(cluster): Logical Replication wird beim Join automatisch eingerichtet
Bisher war der zweite Node nach dem Join zwar im Cluster registriert und
in der UI sichtbar, replizierte aber keine einzige geteilte Tabelle —
dafuer musste jemand manuell `edgeguard-ctl cluster-setup-standby`
ausfuehren. Wer das uebersah, merkte es erst beim Failover: der neue
Primary stand ohne Domains, Backends, Firewall-Regeln und WireGuard-Keys
da. Das ist jetzt Teil des Join-Vorgangs.

Beide Seiten muessen dafuer vorbereitet sein:

1) Primary, beim Erzeugen des Join-Tokens: ein frisch installierter
   Single-Node hat weder Replikations-Rolle noch PUBLICATION noch
   wal_level=logical. Ohne das liefe das spaetere CREATE SUBSCRIPTION in
   ein 404. Der Token wird deshalb erst ausgegeben, nachdem die
   Publisher-Seite steht — inklusive des einmaligen PG-Restarts
   (wal_level ist ein postmaster-Parameter), der bewusst hier passiert,
   solange der Admin danebensteht und noch kein Peer Traffic erwartet.

   WICHTIG dabei: setupReplicationPrimary rotiert bei jedem Lauf das
   Replikations-Passwort (ALTER ROLE … PASSWORD). Auf einem Cluster mit
   bereits angebundenem Subscriber wuerde ein zweiter Token-Klick dessen
   Connection-String ungueltig machen und die Replikation still
   anhalten. Deshalb laeuft die Initialisierung nur, wenn PUBLICATION
   und Secret nicht bereits existieren.

2) Neuer Node, nach erfolgreichem Join: cluster-setup-standby laeuft
   detached (die Initialkopie dauert je nach Datenmenge Minuten), der
   Wizard pollt GET /setup/replication-status und zeigt running/done/
   failed an. Schlaegt es fehl, steht das manuelle Kommando inkl.
   Primary-Host direkt daneben statt nur einer Fehlermeldung.

Beides braucht root (psql als postgres, pg_hba, PG-Restart), die API
laeuft als unprivilegierter edgeguard → Aufruf via sudo mit gepinnten
Regeln. Das einzige variable Argument (Primary-Host) wird vorher gegen
Hostname/IP-Syntax geprueft; der Aufruf laeuft ohne Shell. Test dafuer
liegt bei.

Ausserdem zwei Doku-Korrekturen: architecture.md behauptete,
cluster-join richte die Replikation gleich mit ein (tut es nicht,
clusterjoin.Join macht nur Cert + Registrierung), und der Hinweistext
von cluster-join verwies noch auf "PG-Basebackup + KeyDB, Phase 3.5" —
beides laut Doku laengst verworfen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 11:40:44 +02:00

64 lines
2.2 KiB
Go

package main
import (
"flag"
"fmt"
"os"
"git.netcell-it.de/projekte/edgeguard-native/internal/cluster/clustertls"
"git.netcell-it.de/projekte/edgeguard-native/internal/services/clusterjoin"
)
func cmdClusterJoin(args []string) int {
fs := flag.NewFlagSet("cluster-join", flag.ContinueOnError)
tokenFlag := fs.String("token", "", "cluster join token (eg-join-v1.…)")
insecure := fs.Bool("insecure", false, "skip TLS verification on the primary (bootstrap mode)")
cn := fs.String("cn", "", "subject common name (default: hostname)")
clusterTLSDir := fs.String("tls-dir", clustertls.DefaultDir, "where to write ca.crt + peer.{crt,key}")
fs.SetOutput(os.Stderr)
if err := fs.Parse(args); err != nil {
return 2
}
if fs.NArg() < 1 {
fmt.Fprintln(os.Stderr, "usage: edgeguard-ctl cluster-join <primary-fqdn-or-url> --token <…>")
return 2
}
if *tokenFlag == "" {
fmt.Fprintln(os.Stderr, "edgeguard-ctl cluster-join: --token required")
return 2
}
commonName := *cn
if commonName == "" {
h, _ := os.Hostname()
commonName = h
}
if err := clusterjoin.Join(clusterjoin.Request{
PrimaryFQDN: fs.Arg(0),
Token: *tokenFlag,
CommonName: commonName,
Insecure: *insecure,
TLSDir: *clusterTLSDir,
Version: version,
}); err != nil {
fmt.Fprintf(os.Stderr, "edgeguard-ctl cluster-join: %v\n", err)
return 1
}
primary, _ := clusterjoin.NormalizePrimaryURL(fs.Arg(0))
fmt.Printf("Cluster-Join erfolgreich.\n")
fmt.Printf(" Primary: %s\n", primary)
fmt.Printf(" CN: %s\n", commonName)
fmt.Printf(" Files: %s/{ca.crt,peer.crt,peer.key}\n", *clusterTLSDir)
fmt.Printf("\nNächste Schritte:\n")
fmt.Printf(" 1) sudo edgeguard-ctl cluster-setup-standby %s\n", primary)
fmt.Printf(" → richtet die Logical Replication ein. OHNE diesen Schritt ist der\n")
fmt.Printf(" Node zwar im Cluster, bekommt aber KEINE geteilte Config.\n")
fmt.Printf(" 2) sudo systemctl restart edgeguard-api # lädt das neue Cert in den mTLS-Agent-Listener\n")
fmt.Printf(" 3) Auf dem Primary in der Cluster-UI prüfen ob der neue Peer auftaucht\n")
fmt.Printf("\nHinweis: Beim Join über den Setup-Wizard passiert Schritt 1 automatisch;\n")
fmt.Printf("dieser CLI-Pfad ist der manuelle Weg und braucht ihn explizit.\n")
return 0
}