Files
llmgw/internal/pgha/hba.go
T
Ilker ManapandClaude Opus 5.5 191c65e3db Put etcd and Patroni's API on TLS, and move a running cluster there in maintenance mode
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>
2026-10-04 12:07:17 +02:00

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
}