simplify production database configuration

This commit is contained in:
brother7 committed 2026-08-16 12:40:43 +08:00
1 parent 8cb8b5e463
commit b26f9679ab
17 files changed
+199 -234

No files matched your search

+25 -23
View File
@@ -49,7 +49,10 @@ func ParseConfig(getenv Getenv, readFile ReadFile) (Config, error) {
ConnectionTimeout: 10 * time.Second,
StatementTimeout: 30 * time.Second,
ApplicationName: "zhinian-go",
SSLMode: SSLDisable,
// DATABASE_URL is the only database setting. Production defaults to
// full TLS verification; sslrootcert may be supplied in the URL when
// the RDS CA is not part of the container's system trust store.
SSLMode: SSLVerifyFull,
}
backend := strings.ToLower(strings.TrimSpace(getenv("ZHINIAN_DATA_BACKEND")))
@@ -93,37 +96,36 @@ func ParseConfig(getenv Getenv, readFile ReadFile) (Config, error) {
if parsed.Scheme != "postgres" && parsed.Scheme != "postgresql" {
return Config{}, fmt.Errorf("DATABASE_URL must use the postgres:// or postgresql:// scheme")
}
for key := range parsed.Query() {
if strings.HasPrefix(strings.ToLower(key), "ssl") {
return Config{}, fmt.Errorf("DATABASE_URL must not contain SSL query parameters (%s); use DATABASE_SSL_MODE and DATABASE_CA_CERT_PATH", key)
query := parsed.Query()
for key := range query {
lower := strings.ToLower(key)
if strings.HasPrefix(lower, "ssl") && lower != "sslmode" && lower != "sslrootcert" {
return Config{}, fmt.Errorf("DATABASE_URL contains unsupported SSL query parameter %s; use sslmode and optional sslrootcert", key)
}
}
mode := strings.ToLower(strings.TrimSpace(getenv("DATABASE_SSL_MODE")))
if mode != "" {
if mode := strings.ToLower(strings.TrimSpace(query.Get("sslmode"))); mode != "" {
cfg.SSLMode = SSLMode(mode)
}
switch cfg.SSLMode {
case SSLDisable:
case SSLVerifyFull:
path := strings.TrimSpace(getenv("DATABASE_CA_CERT_PATH"))
if path == "" {
return Config{}, fmt.Errorf("DATABASE_CA_CERT_PATH is required when DATABASE_SSL_MODE=verify-full")
cfg.TLSConfig = &tls.Config{MinVersion: tls.VersionTLS12}
if path := strings.TrimSpace(query.Get("sslrootcert")); path != "" {
if readFile == nil {
return Config{}, fmt.Errorf("a certificate reader is required when DATABASE_URL contains sslrootcert")
}
pem, readErr := readFile(path)
if readErr != nil {
return Config{}, fmt.Errorf("read DATABASE_URL sslrootcert: %w", readErr)
}
roots := x509.NewCertPool()
if !roots.AppendCertsFromPEM(pem) {
return Config{}, fmt.Errorf("DATABASE_URL sslrootcert does not contain a valid CA certificate")
}
cfg.TLSConfig.RootCAs = roots
}
if readFile == nil {
return Config{}, fmt.Errorf("CA certificate reader is required")
}
pem, readErr := readFile(path)
if readErr != nil {
return Config{}, fmt.Errorf("read DATABASE_CA_CERT_PATH: %w", readErr)
}
roots := x509.NewCertPool()
if !roots.AppendCertsFromPEM(pem) {
return Config{}, fmt.Errorf("DATABASE_CA_CERT_PATH does not contain a valid CA certificate")
}
cfg.TLSConfig = &tls.Config{RootCAs: roots, MinVersion: tls.VersionTLS12}
default:
return Config{}, fmt.Errorf("DATABASE_SSL_MODE must be 'disable' or 'verify-full'")
return Config{}, fmt.Errorf("DATABASE_URL sslmode must be 'disable' or 'verify-full'")
}
return cfg, nil
}
+12 -13
View File
@@ -7,6 +7,7 @@ import (
"crypto/x509/pkix"
"encoding/pem"
"math/big"
"net/url"
"os"
"path/filepath"
"strings"
@@ -38,14 +39,13 @@ func TestParseConfigRequiresPostgresURL(t *testing.T) {
}
}
func TestParseConfigAcceptsOnlyPostgresSchemesAndRejectsSSLQueryParameters(t *testing.T) {
func TestParseConfigAcceptsOnlyPostgresSchemesAndRejectsUnsupportedSSLQueryParameters(t *testing.T) {
tests := []struct {
name string
url string
}{
{name: "wrong scheme", url: "https://db.example/app"},
{name: "sslmode", url: "postgres://db.example/app?sslmode=require"},
{name: "mixed case ssl parameter", url: "postgresql://db.example/app?SSLcert=x"},
{name: "mixed case unsupported ssl parameter", url: "postgresql://db.example/app?SSLcert=x"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -60,7 +60,7 @@ func TestParseConfigAcceptsOnlyPostgresSchemesAndRejectsSSLQueryParameters(t *te
}
}
func TestParseConfigBuildsVerifyFullTLSFromCA(t *testing.T) {
func TestParseConfigBuildsVerifyFullTLSFromURLCA(t *testing.T) {
dir := t.TempDir()
caPath := filepath.Join(dir, "ca.pem")
const ca = "-----BEGIN CERTIFICATE-----\nMIIB\n-----END CERTIFICATE-----\n"
@@ -71,10 +71,8 @@ func TestParseConfigBuildsVerifyFullTLSFromCA(t *testing.T) {
return []byte(ca), nil
}
_, err := ParseConfig(env(map[string]string{
"ZHINIAN_DATA_BACKEND": "postgres",
"DATABASE_URL": "postgresql://db.example/app",
"DATABASE_SSL_MODE": "verify-full",
"DATABASE_CA_CERT_PATH": caPath,
"ZHINIAN_DATA_BACKEND": "postgres",
"DATABASE_URL": "postgresql://db.example/app?sslmode=verify-full&sslrootcert=" + url.QueryEscape(caPath),
}), read)
if err == nil || !strings.Contains(err.Error(), "CA certificate") {
t.Fatalf("ParseConfig() error = %v, want invalid CA certificate error", err)
@@ -97,10 +95,8 @@ func TestParseConfigVerifyFullBuildsRootsWithoutDisablingVerification(t *testing
}
ca := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})
cfg, err := ParseConfig(env(map[string]string{
"ZHINIAN_DATA_BACKEND": "postgres",
"DATABASE_URL": "postgresql://db.example/app",
"DATABASE_SSL_MODE": "verify-full",
"DATABASE_CA_CERT_PATH": "/ca.pem",
"ZHINIAN_DATA_BACKEND": "postgres",
"DATABASE_URL": "postgresql://db.example/app?sslmode=verify-full&sslrootcert=%2Fca.pem",
}), func(string) ([]byte, error) { return ca, nil })
if err != nil {
t.Fatalf("ParseConfig() error = %v", err)
@@ -124,9 +120,12 @@ func TestParseConfigDefaultsAndNumericValidation(t *testing.T) {
if cfg.PoolMax != 10 || cfg.IdleTimeout != 30*time.Second || cfg.ConnectionTimeout != 10*time.Second || cfg.StatementTimeout != 30*time.Second {
t.Fatalf("unexpected defaults: %+v", cfg)
}
if cfg.ApplicationName != "zhinian-go" || cfg.SSLMode != SSLDisable {
if cfg.ApplicationName != "zhinian-go" || cfg.SSLMode != SSLVerifyFull {
t.Fatalf("unexpected identity/TLS defaults: %+v", cfg)
}
if cfg.TLSConfig == nil || cfg.TLSConfig.InsecureSkipVerify {
t.Fatal("default PostgreSQL configuration must verify the server certificate")
}
for name, value := range map[string]string{
"DATABASE_POOL_MAX": "0",
+2 -2
View File
@@ -42,7 +42,7 @@ func Open(ctx context.Context, config Config) (*Module, error) {
poolConfig.ConnConfig.Fallbacks = nil
case SSLVerifyFull:
if config.TLSConfig == nil {
return nil, fmt.Errorf("TLS configuration is required when DATABASE_SSL_MODE=verify-full")
return nil, fmt.Errorf("TLS configuration is required when DATABASE_URL sslmode=verify-full")
}
tlsConfig := config.TLSConfig.Clone()
if tlsConfig.ServerName == "" {
@@ -51,7 +51,7 @@ func Open(ctx context.Context, config Config) (*Module, error) {
poolConfig.ConnConfig.TLSConfig = tlsConfig
poolConfig.ConnConfig.Fallbacks = nil
default:
return nil, fmt.Errorf("unsupported DATABASE_SSL_MODE %q", config.SSLMode)
return nil, fmt.Errorf("unsupported DATABASE_URL sslmode %q", config.SSLMode)
}
pool, err := pgxpool.NewWithConfig(ctx, poolConfig)
if err != nil {
+2 -4
View File
@@ -37,8 +37,6 @@ func TestOpenPostgresDoesNotProbeBeforeReadiness(t *testing.T) {
func TestParseConfigIgnoresPostgresTLSSettingsForLocalBackend(t *testing.T) {
cfg, err := ParseConfig(env(map[string]string{
"ZHINIAN_DATA_BACKEND": "local",
"DATABASE_SSL_MODE": "verify-full",
"DATABASE_CA_CERT_PATH": "/missing/ca.pem",
"DATABASE_POOL_MAX": "0",
"DATABASE_CONNECTION_TIMEOUT_MS": "invalid",
}), nil)
@@ -56,7 +54,7 @@ func TestOpenPostgresRejectsInvalidConfiguredTLSMode(t *testing.T) {
DatabaseURL: "postgresql://app:secret@db.example/app",
SSLMode: SSLMode("prefer"),
})
if err == nil || !strings.Contains(err.Error(), "DATABASE_SSL_MODE") {
t.Fatalf("Open() error = %v", err)
if err == nil || !strings.Contains(err.Error(), "unsupported DATABASE_URL sslmode") {
t.Fatalf("Open() error = %v, want DATABASE_URL sslmode error", err)
}
}