1212package pgsqlHealth
1313
1414import (
15+ "crypto/tls"
16+ "crypto/x509"
1517 "database/sql"
1618 "errors"
1719 "fmt"
20+ "net/http"
21+ "net/url"
1822 "os"
1923 "strings"
24+ "time"
2025
2126 _ "github.com/lib/pq"
2227 "github.com/rs/zerolog/log"
2328 "gopkg.in/yaml.v3"
2429)
2530
31+ var patroniHTTPClient * http.Client
32+
33+ type patroniConf struct {
34+ Name string `json:"name"`
35+ Restapi patroniRestAPI `yaml:"restapi"`
36+ }
37+
38+ type patroniRestAPI struct {
39+ ConnectAddress string `yaml:"connect_address"`
40+ CertFile string `yaml:"certfile"`
41+ KeyFile string `yaml:"keyfile"`
42+ CAFile string `yaml:"cafile"`
43+ }
44+
2645// getPatroniUrl reads the Patroni configuration file and returns the REST API
2746// connection address for the Patroni cluster
2847func getPatroniUrl () (string , error ) {
@@ -33,13 +52,6 @@ func getPatroniUrl() (string, error) {
3352 return "" , err
3453 }
3554
36- // Create a struct to hold the YAML data
37- type patroniConf struct {
38- Name string `json:"name"`
39- Restapi struct {
40- ConnectAddress string `yaml:"connect_address"`
41- } `yaml:"restapi"`
42- }
4355 var patroni patroniConf
4456 // Unmarshal the YAML data into the struct
4557 err = yaml .Unmarshal (data , & patroni )
@@ -49,7 +61,41 @@ func getPatroniUrl() (string, error) {
4961 }
5062 nodeName = patroni .Name
5163
52- return patroni .Restapi .ConnectAddress , nil
64+ connectAddress := strings .TrimSpace (patroni .Restapi .ConnectAddress )
65+ if connectAddress == "" {
66+ return "" , errors .New ("patroni restapi connect_address is empty" )
67+ }
68+
69+ tlsConfigured := patroni .Restapi .CertFile != "" || patroni .Restapi .KeyFile != "" || patroni .Restapi .CAFile != ""
70+ hasScheme := strings .HasPrefix (connectAddress , "http://" ) || strings .HasPrefix (connectAddress , "https://" )
71+
72+ if ! hasScheme {
73+ if tlsConfigured {
74+ connectAddress = "https://" + connectAddress
75+ } else {
76+ connectAddress = "http://" + connectAddress
77+ }
78+ }
79+
80+ u , err := url .Parse (connectAddress )
81+ if err != nil || u .Scheme == "" || u .Host == "" {
82+ return "" , fmt .Errorf ("invalid patroni connect_address: %s" , connectAddress )
83+ }
84+
85+ // Keep only scheme://host[:port] portion to avoid double slashes when adding paths
86+ baseURL := fmt .Sprintf ("%s://%s" , u .Scheme , u .Host )
87+ if u .Path != "" && u .Path != "/" {
88+ baseURL = fmt .Sprintf ("%s%s" , baseURL , strings .TrimSuffix (u .Path , "/" ))
89+ }
90+
91+ client , err := buildPatroniHTTPClient (patroni .Restapi , u .Hostname (), tlsConfigured )
92+ if err != nil {
93+ log .Error ().Err (err ).Str ("component" , "pgsqlHealth" ).Str ("operation" , "getPatroniUrl" ).Str ("action" , "build_patroni_http_client_failed" ).Msg ("couldn't build patroni http client with provided certificates" )
94+ return "" , err
95+ }
96+ patroniHTTPClient = client
97+
98+ return baseURL , nil
5399}
54100
55101// Connect establishes a connection to the PostgreSQL database
@@ -127,3 +173,59 @@ func Connect() error {
127173 Connection = db
128174 return nil
129175}
176+
177+ func buildPatroniHTTPClient (restapi patroniRestAPI , serverName string , tlsConfigured bool ) (* http.Client , error ) {
178+ if ! tlsConfigured {
179+ return & http.Client {Timeout : 10 * time .Second }, nil
180+ }
181+
182+ tlsConfig := & tls.Config {
183+ MinVersion : tls .VersionTLS12 ,
184+ }
185+
186+ if serverName != "" {
187+ tlsConfig .ServerName = serverName
188+ }
189+
190+ if restapi .CAFile != "" {
191+ caData , err := os .ReadFile (restapi .CAFile )
192+ if err != nil {
193+ return nil , fmt .Errorf ("couldn't read patroni CA file: %w" , err )
194+ }
195+
196+ rootCAs , err := x509 .SystemCertPool ()
197+ if err != nil || rootCAs == nil {
198+ rootCAs = x509 .NewCertPool ()
199+ }
200+
201+ if ok := rootCAs .AppendCertsFromPEM (caData ); ! ok {
202+ return nil , fmt .Errorf ("failed to append CA certificates from %s" , restapi .CAFile )
203+ }
204+
205+ tlsConfig .RootCAs = rootCAs
206+ }
207+
208+ if restapi .CertFile != "" && restapi .KeyFile != "" {
209+ clientCert , err := tls .LoadX509KeyPair (restapi .CertFile , restapi .KeyFile )
210+ if err != nil {
211+ return nil , fmt .Errorf ("couldn't load patroni client certificate or key: %w" , err )
212+ }
213+ tlsConfig .Certificates = []tls.Certificate {clientCert }
214+ }
215+
216+ transport := & http.Transport {
217+ TLSClientConfig : tlsConfig ,
218+ }
219+
220+ return & http.Client {
221+ Timeout : 10 * time .Second ,
222+ Transport : transport ,
223+ }, nil
224+ }
225+
226+ func getPatroniHTTPClient () * http.Client {
227+ if patroniHTTPClient != nil {
228+ return patroniHTTPClient
229+ }
230+ return & http.Client {Timeout : 10 * time .Second }
231+ }
0 commit comments