mirror of
https://github.com/jeremyd/ergo.git
synced 2026-08-01 11:43:06 -07:00
postgres: accept libpq URIs (#2347)
This commit is contained in:
committed by
GitHub
parent
5fd8ed8ed6
commit
69efae2c2e
+7
-5
@@ -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:
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user