Members' etcd speaks TLS to peers and clients with a certificate from the cluster's authority and asks clients for one; Patroni reaches it with the member's certificate and serves its API over TLS. A running plain-HTTP cluster is moved with Patroni paused, so PostgreSQL serves throughout: Patroni's lease-free keys are carried over to the new etcd. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
78 lines
2.4 KiB
Go
78 lines
2.4 KiB
Go
package pgha
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"slices"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// ClientLine is the access rule that lets a control plane at ip in.
|
|
func ClientLine(ip string) string {
|
|
return fmt.Sprintf("host %s %s %s/32 scram-sha-256", Database, Role, ip)
|
|
}
|
|
|
|
// AllowClient lets a control plane at ip into the cluster's database: one
|
|
// change to the cluster's shared configuration through Patroni's API, which
|
|
// every member applies. It says whether anything changed.
|
|
//
|
|
// A cluster set up before the rules moved into the shared configuration
|
|
// keeps them in each member's own file, which this cannot reach; that is
|
|
// said, with what to add, rather than half done.
|
|
//
|
|
// password is what Patroni's API asks for before a change (RestUser); a
|
|
// cluster whose rules are hostssl lets the new control plane in by TLS too.
|
|
func AllowClient(ctx context.Context, hosts []string, ip, password string) (bool, error) {
|
|
var lastErr error
|
|
for _, h := range hosts {
|
|
cfg, err := getConfig(ctx, h)
|
|
if err != nil {
|
|
lastErr = err
|
|
continue
|
|
}
|
|
var hba []string
|
|
if pg, ok := cfg["postgresql"].(map[string]any); ok {
|
|
if list, ok := pg["pg_hba"].([]any); ok {
|
|
for _, l := range list {
|
|
if s, ok := l.(string); ok {
|
|
hba = append(hba, s)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if len(hba) == 0 {
|
|
return false, fmt.Errorf("this cluster keeps its access rules in each member's "+
|
|
"/etc/patroni/config.yml, not in its shared configuration: add %q to pg_hba there "+
|
|
"on every member, and run `patronictl reload llmgw`", ClientLine(ip))
|
|
}
|
|
line := ClientLine(ip)
|
|
if slices.ContainsFunc(hba, func(l string) bool { return strings.HasPrefix(l, "hostssl ") }) {
|
|
line = hostssl(line)
|
|
}
|
|
if slices.Contains(hba, line) {
|
|
return false, nil
|
|
}
|
|
if err := patchConfig(ctx, h, password,
|
|
map[string]any{"postgresql": map[string]any{"pg_hba": append(hba, line)}}); err != nil {
|
|
return false, fmt.Errorf("Patroni on %s: %w", h, err)
|
|
}
|
|
return true, nil
|
|
}
|
|
return false, fmt.Errorf("no member's Patroni answered: %w", lastErr)
|
|
}
|
|
|
|
func getConfig(ctx context.Context, host string) (map[string]any, error) {
|
|
b, err := restJSON(ctx, host, http.MethodGet, "/config", nil, "", 10*time.Second)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var cfg map[string]any
|
|
if err := json.Unmarshal(b, &cfg); err != nil {
|
|
return nil, fmt.Errorf("%s /config: %w", host, err)
|
|
}
|
|
return cfg, nil
|
|
}
|