postgres: accept libpq URIs (#2347)

This commit is contained in:
Shivaram Lingamneni
2026-03-05 11:58:09 -08:00
committed by GitHub
parent 5fd8ed8ed6
commit 69efae2c2e
5 changed files with 185 additions and 45 deletions
+7 -5
View File
@@ -885,9 +885,16 @@ datastore:
# if socket-path is set, it will be used instead of host:port
# PostgreSQL uses the socket directory, not the socket file path
#socket-path: "/var/run/postgresql"
# PostgreSQL SSL/TLS configuration:
ssl-mode: "disable" # options: disable, require, verify-ca, verify-full
#ssl-cert: "/path/to/client-cert.pem"
#ssl-key: "/path/to/client-key.pem"
#ssl-root-cert: "/path/to/ca-cert.pem"
user: "ergo"
password: "hunter2"
history-database: "ergo_history"
# uri takes a postgresql:// (libpq) URI, overriding the above parameters if present:
# uri: "postgresql://ergo:hunter2@localhost/ergo_history"
timeout: 3s
max-conns: 4
# this may be necessary to prevent middleware from closing your connections:
@@ -896,11 +903,6 @@ datastore:
#application-name: "ergo"
# timeout for establishing initial connections to PostgreSQL:
#connect-timeout: 10s
# PostgreSQL SSL/TLS configuration:
ssl-mode: "disable" # options: disable, require, verify-ca, verify-full
#ssl-cert: "/path/to/client-cert.pem"
#ssl-key: "/path/to/client-key.pem"
#ssl-root-cert: "/path/to/ca-cert.pem"
# languages config
languages:
+60
View File
@@ -4,6 +4,8 @@
package postgres
import (
"fmt"
"net/url"
"time"
)
@@ -33,8 +35,66 @@ type Config struct {
SSLCert string `yaml:"ssl-cert"` // client certificate path
SSLKey string `yaml:"ssl-key"` // client key path
SSLRootCert string `yaml:"ssl-root-cert"` // CA certificate path
URI string `yaml:"uri"` // libpq postgresql:// URI overriding the above
// XXX these are copied from elsewhere in the config:
ExpireTime time.Duration
TrackAccountMessages bool
}
func (config *Config) buildURI() (string, error) {
u := &url.URL{
Scheme: "postgresql",
Path: "/" + config.HistoryDatabase,
}
q := url.Values{}
if config.SocketPath != "" {
// For Unix sockets, pgx uses host as a query parameter
q.Set("host", config.SocketPath)
if config.User != "" || config.Password != "" {
u.User = url.UserPassword(config.User, config.Password)
}
} else {
// TCP connection
port := config.Port
if port == 0 {
port = 5432
}
host := config.Host
if host == "" {
host = "localhost"
}
u.Host = fmt.Sprintf("%s:%d", host, port)
if config.User != "" || config.Password != "" {
u.User = url.UserPassword(config.User, config.Password)
}
sslMode := config.SSLMode
if sslMode == "" {
sslMode = "disable"
}
q.Set("sslmode", sslMode)
if config.SSLCert != "" {
q.Set("sslcert", config.SSLCert)
}
if config.SSLKey != "" {
q.Set("sslkey", config.SSLKey)
}
if config.SSLRootCert != "" {
q.Set("sslrootcert", config.SSLRootCert)
}
}
if config.ApplicationName != "" {
q.Set("application_name", config.ApplicationName)
}
if config.ConnectTimeout != 0 {
q.Set("connect_timeout", fmt.Sprintf("%d", int(config.ConnectTimeout.Seconds())))
}
u.RawQuery = q.Encode()
return u.String(), nil
}
+84
View File
@@ -0,0 +1,84 @@
// Copyright (c) 2020 Shivaram Lingamneni
// released under the MIT license
package postgres
import (
"testing"
"time"
)
func testBuildURI(t *testing.T, config Config, expected string) {
t.Helper()
uri, err := config.buildURI()
if err != nil {
t.Fatal(err)
}
if uri != expected {
t.Errorf("got %q, want %q", uri, expected)
}
}
func TestBuildURITCP(t *testing.T) {
testBuildURI(t, Config{
Host: "db.example.com",
Port: 5432,
User: "ergo",
Password: "secret",
HistoryDatabase: "ergo_history",
}, "postgresql://ergo:secret@db.example.com:5432/ergo_history?sslmode=disable")
}
func TestBuildURIDefaultPort(t *testing.T) {
testBuildURI(t, Config{
Host: "localhost",
HistoryDatabase: "ergo_history",
}, "postgresql://localhost:5432/ergo_history?sslmode=disable")
}
func TestBuildURIDefaultHost(t *testing.T) {
testBuildURI(t, Config{
HistoryDatabase: "ergo_history",
}, "postgresql://localhost:5432/ergo_history?sslmode=disable")
}
func TestBuildURISSLMode(t *testing.T) {
testBuildURI(t, Config{
Host: "db.example.com",
Port: 5432,
HistoryDatabase: "ergo_history",
SSLMode: "verify-full",
SSLCert: "/etc/ssl/client.crt",
SSLKey: "/etc/ssl/client.key",
SSLRootCert: "/etc/ssl/ca.crt",
}, "postgresql://db.example.com:5432/ergo_history?sslcert=%2Fetc%2Fssl%2Fclient.crt&sslkey=%2Fetc%2Fssl%2Fclient.key&sslmode=verify-full&sslrootcert=%2Fetc%2Fssl%2Fca.crt")
}
func TestBuildURIUnixSocket(t *testing.T) {
testBuildURI(t, Config{
SocketPath: "/var/run/postgresql",
User: "ergo",
Password: "secret",
HistoryDatabase: "ergo_history",
}, "postgresql://ergo:secret@/ergo_history?host=%2Fvar%2Frun%2Fpostgresql")
}
func TestBuildURISpecialCharsInPassword(t *testing.T) {
testBuildURI(t, Config{
Host: "db.example.com",
Port: 5432,
User: "ergo",
Password: "p@ss:w/ord?#&=",
HistoryDatabase: "ergo_history",
}, "postgresql://ergo:p%40ss%3Aw%2Ford%3F%23&=@db.example.com:5432/ergo_history?sslmode=disable")
}
func TestBuildURIOptionalParams(t *testing.T) {
testBuildURI(t, Config{
Host: "db.example.com",
Port: 5433,
HistoryDatabase: "ergo_history",
ApplicationName: "ergo",
ConnectTimeout: 30 * time.Second,
}, "postgresql://db.example.com:5433/ergo_history?application_name=ergo&connect_timeout=30&sslmode=disable")
}
+27 -35
View File
@@ -11,6 +11,7 @@ import (
"encoding/json"
"fmt"
"io"
"net/url"
"runtime/debug"
"slices"
"strings"
@@ -93,45 +94,20 @@ func (pg *PostgreSQL) getExpireTime() (expireTime time.Duration) {
func (pg *PostgreSQL) open() (err error) {
// Build PostgreSQL connection string
var connString string
if pg.config.SocketPath != "" {
// PostgreSQL uses host parameter for Unix socket directory
connString = fmt.Sprintf("host=%s user=%s password=%s dbname=%s",
pg.config.SocketPath, pg.config.User, pg.config.Password, pg.config.HistoryDatabase)
uri := pg.config.URI
if uri != "" {
uri, err = pg.mungeURI(uri)
if err != nil {
return err
}
} else {
// TCP connection
port := pg.config.Port
if port == 0 {
port = 5432 // Default PostgreSQL port
}
sslMode := pg.config.SSLMode
if sslMode == "" {
sslMode = "disable"
}
connString = fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
pg.config.Host, port, pg.config.User, pg.config.Password, pg.config.HistoryDatabase, sslMode)
// Add SSL certificate paths if provided
if pg.config.SSLCert != "" {
connString += fmt.Sprintf(" sslcert=%s", pg.config.SSLCert)
}
if pg.config.SSLKey != "" {
connString += fmt.Sprintf(" sslkey=%s", pg.config.SSLKey)
}
if pg.config.SSLRootCert != "" {
connString += fmt.Sprintf(" sslrootcert=%s", pg.config.SSLRootCert)
uri, err = pg.config.buildURI()
if err != nil {
return err
}
}
// Add optional PostgreSQL-specific parameters
if pg.config.ApplicationName != "" {
connString += fmt.Sprintf(" application_name=%s", pg.config.ApplicationName)
}
if pg.config.ConnectTimeout != 0 {
connString += fmt.Sprintf(" connect_timeout=%d", int(pg.config.ConnectTimeout.Seconds()))
}
pg.db, err = sql.Open("pgx", connString)
pg.db, err = sql.Open("pgx", uri)
if err != nil {
return err
}
@@ -160,6 +136,22 @@ func (pg *PostgreSQL) open() (err error) {
return nil
}
func (pg *PostgreSQL) mungeURI(uriStr string) (result string, err error) {
uri, err := url.Parse(uriStr)
if err != nil {
return "", fmt.Errorf("could not parse postgres URI: %w", err)
}
values := uri.Query()
if !values.Has("application_name") {
values.Set("application_name", pg.config.ApplicationName)
}
if !values.Has("connect_timeout") {
values.Set("connect_timeout", fmt.Sprintf("%d", int(pg.config.ConnectTimeout.Seconds())))
}
uri.RawQuery = values.Encode()
return uri.String(), nil
}
func (pg *PostgreSQL) fixSchemas() (err error) {
_, err = pg.db.Exec(`CREATE TABLE IF NOT EXISTS metadata (
key_name VARCHAR(32) PRIMARY KEY,
+7 -5
View File
@@ -856,9 +856,16 @@ datastore:
# if socket-path is set, it will be used instead of host:port
# PostgreSQL uses the socket directory, not the socket file path
#socket-path: "/var/run/postgresql"
# PostgreSQL SSL/TLS configuration:
ssl-mode: "disable" # options: disable, require, verify-ca, verify-full
#ssl-cert: "/path/to/client-cert.pem"
#ssl-key: "/path/to/client-key.pem"
#ssl-root-cert: "/path/to/ca-cert.pem"
user: "ergo"
password: "hunter2"
history-database: "ergo_history"
# uri takes a postgresql:// (libpq) URI, overriding the above parameters if present:
# uri: "postgresql://ergo:hunter2@localhost/ergo_history"
timeout: 3s
max-conns: 4
# this may be necessary to prevent middleware from closing your connections:
@@ -867,11 +874,6 @@ datastore:
#application-name: "ergo"
# timeout for establishing initial connections to PostgreSQL:
#connect-timeout: 10s
# PostgreSQL SSL/TLS configuration:
ssl-mode: "disable" # options: disable, require, verify-ca, verify-full
#ssl-cert: "/path/to/client-cert.pem"
#ssl-key: "/path/to/client-key.pem"
#ssl-root-cert: "/path/to/ca-cert.pem"
# languages config
languages: