Skip to content

Commit 792fee5

Browse files
committed
Support TLS patroni endpoints
1 parent 4d497b9 commit 792fee5

File tree

4 files changed

+118
-12
lines changed

4 files changed

+118
-12
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,7 @@ coverage.html
1010
plugins/
1111
common/health/pluginpb
1212
*.coverprofile
13+
# Cache dirs
14+
.cache/
15+
.gocache/
16+
.modcache/

pgsqlHealth/cluster.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,8 @@ func checkPatroniService() {
106106
// getClusterStatus retrieves the cluster status from the Patroni API
107107
// and returns the current and previous cluster statuses
108108
func getClusterStatus(patroniApiUrl string) (*Response, *Response) { // Added parameter
109-
client := &http.Client{Timeout: time.Second * 10}
110-
clusterURL := "http://" + patroniApiUrl + "/cluster" // Use passed parameter
109+
client := getPatroniHTTPClient()
110+
clusterURL := strings.TrimSuffix(patroniApiUrl, "/") + "/cluster" // Use passed parameter
111111

112112
resp, err := client.Get(clusterURL)
113113
if err != nil {

pgsqlHealth/connection.go

Lines changed: 110 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,36 @@
1212
package pgsqlHealth
1313

1414
import (
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
2847
func 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+
}

pgsqlHealth/main.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import (
77
"errors"
88
"fmt"
99
"io"
10-
"net/http"
1110
"os"
1211
"os/exec"
1312
"strings"
@@ -243,7 +242,8 @@ func Main(cmd *cobra.Command, args []string) {
243242
}
244243

245244
// Get patroni role
246-
patroniRole, err := http.Get("http://" + patroniApiUrl + "/patroni")
245+
patroniClient := getPatroniHTTPClient()
246+
patroniRole, err := patroniClient.Get(strings.TrimSuffix(patroniApiUrl, "/") + "/patroni")
247247
if err != nil {
248248
log.Error().Err(err).Str("component", "pgsqlHealth").Str("operation", "Main").Str("action", "get_patroni_role_failed").Msg("Error getting patroni role")
249249
} else {

0 commit comments

Comments
 (0)