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>
54 lines
1.1 KiB
Go
54 lines
1.1 KiB
Go
package handlers
|
|
|
|
import "testing"
|
|
|
|
// validPrimaryHost bewacht das einzige variable Argument einer sudo-Regel
|
|
// (`edgeguard-ctl cluster-setup-standby *`). Der Aufruf laeuft zwar ohne
|
|
// Shell, aber die Pruefung soll trotzdem halten was sie verspricht.
|
|
func TestValidPrimaryHost(t *testing.T) {
|
|
valid := []string{
|
|
"utm-1.netcell-it.de",
|
|
"primary",
|
|
"10.0.5.1",
|
|
"89.163.205.6",
|
|
"2001:db8::1",
|
|
"a-b-c.example.com",
|
|
}
|
|
for _, h := range valid {
|
|
if !validPrimaryHost(h) {
|
|
t.Errorf("validPrimaryHost(%q) = false, erwartet true", h)
|
|
}
|
|
}
|
|
|
|
invalid := []string{
|
|
"",
|
|
" ",
|
|
"host; rm -rf /",
|
|
"host && reboot",
|
|
"host|tee",
|
|
"host$(id)",
|
|
"host`id`",
|
|
"--tls-dir=/tmp/evil",
|
|
"host with space",
|
|
"host\nsecond-line",
|
|
"..",
|
|
"host..example.com",
|
|
"/etc/passwd",
|
|
}
|
|
for _, h := range invalid {
|
|
if validPrimaryHost(h) {
|
|
t.Errorf("validPrimaryHost(%q) = true, erwartet false", h)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestValidPrimaryHostRejectsOverlongName(t *testing.T) {
|
|
long := make([]byte, 254)
|
|
for i := range long {
|
|
long[i] = 'a'
|
|
}
|
|
if validPrimaryHost(string(long)) {
|
|
t.Error("Hostname > 253 Zeichen muss abgelehnt werden")
|
|
}
|
|
}
|