Skip to content

Commit 442ec56

Browse files
cbandyandrewlecuyer
authored andcommitted
Mount PgBouncer certificates from one projected volume
PgBouncer v1.16 is able to reload TLS certificates, but we mounted the files outside of the one directory monitored by the reload sidecar. Rather than create more sidecars, add the certificates to the existing projected volume. Issue: [sc-12361] See: https://www.pgbouncer.org/changelog.html#pgbouncer-116x See: pgbouncer/pgbouncer@cfbb8a3ae7a0
1 parent 1cd52a1 commit 442ec56

File tree

7 files changed

+184
-155
lines changed

7 files changed

+184
-155
lines changed

docs/content/tutorial/administrative-tasks.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,9 @@ When changing the PostgreSQL certificate authority, make sure to update
5252
[`customReplicationTLSSecret`]({{< relref "/tutorial/customize-cluster.md" >}}#customize-tls) as well.
5353
{{% /notice %}}
5454

55-
PgBouncer needs to be restarted after its certificates change.
55+
PGO automatically notifies PgBouncer when there are changes to the contents of
56+
PgBouncer certificate Secrets. Recent PgBouncer versions load those changes
57+
without downtime, but versions prior to 1.16.0 need to be restarted manually.
5658
There are a few ways to do it:
5759

5860
1. Store the new certificates in a new Secret. Edit the PostgresCluster object

internal/pgbouncer/assertions_test.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,17 @@
1616
package pgbouncer
1717

1818
import (
19+
"strings"
20+
1921
"gotest.tools/v3/assert/cmp"
2022
"sigs.k8s.io/yaml"
2123
)
2224

23-
func marshalEquals(actual interface{}, expected string) cmp.Comparison {
25+
// marshalMatches converts actual to YAML and compares that to expected.
26+
func marshalMatches(actual interface{}, expected string) cmp.Comparison {
2427
b, err := yaml.Marshal(actual)
25-
return func() cmp.Result {
26-
if err != nil {
27-
return cmp.ResultFromError(err)
28-
}
29-
return cmp.DeepEqual(string(b), expected)()
28+
if err != nil {
29+
return func() cmp.Result { return cmp.ResultFromError(err) }
3030
}
31+
return cmp.DeepEqual(string(b), strings.Trim(expected, "\t\n")+"\n")
3132
}

internal/pgbouncer/certificates.go

Lines changed: 71 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -20,23 +20,24 @@ import (
2020
)
2121

2222
const (
23-
certBackendDirectory = configDirectory + "/~postgres-operator-backend"
24-
certFrontendDirectory = configDirectory + "/~postgres-operator-frontend"
23+
tlsAuthoritySecretKey = "ca.crt"
24+
tlsCertificateSecretKey = corev1.TLSCertKey
25+
tlsPrivateKeySecretKey = corev1.TLSPrivateKeyKey
2526

26-
certBackendAuthorityAbsolutePath = certBackendDirectory + "/" + certBackendAuthorityProjectionPath
27-
certBackendAuthorityProjectionPath = "ca.crt"
27+
certBackendAuthorityAbsolutePath = configDirectory + "/" + certBackendAuthorityProjectionPath
28+
certBackendAuthorityProjectionPath = "~postgres-operator/backend-ca.crt"
2829

29-
certFrontendAuthorityAbsolutePath = certFrontendDirectory + "/" + certFrontendAuthorityProjectionPath
30-
certFrontendPrivateKeyAbsolutePath = certFrontendDirectory + "/" + certFrontendPrivateKeyProjectionPath
31-
certFrontendAbsolutePath = certFrontendDirectory + "/" + certFrontendProjectionPath
30+
certFrontendAuthorityAbsolutePath = configDirectory + "/" + certFrontendAuthorityProjectionPath
31+
certFrontendPrivateKeyAbsolutePath = configDirectory + "/" + certFrontendPrivateKeyProjectionPath
32+
certFrontendAbsolutePath = configDirectory + "/" + certFrontendProjectionPath
3233

33-
certFrontendAuthorityProjectionPath = "ca.crt"
34-
certFrontendPrivateKeyProjectionPath = "tls.key"
35-
certFrontendProjectionPath = "tls.crt"
34+
certFrontendAuthorityProjectionPath = "~postgres-operator/frontend-ca.crt"
35+
certFrontendPrivateKeyProjectionPath = "~postgres-operator/frontend-tls.key"
36+
certFrontendProjectionPath = "~postgres-operator/frontend-tls.crt"
3637

37-
certFrontendAuthoritySecretKey = "pgbouncer-frontend.ca-roots" // #nosec G101 this is a name, not a credential
38-
certFrontendPrivateKeySecretKey = "pgbouncer-frontend.key" // #nosec G101 this is a name, not a credential
39-
certFrontendSecretKey = "pgbouncer-frontend.crt" // #nosec G101 this is a name, not a credential
38+
certFrontendAuthoritySecretKey = "pgbouncer-frontend.ca-roots"
39+
certFrontendPrivateKeySecretKey = "pgbouncer-frontend.key"
40+
certFrontendSecretKey = "pgbouncer-frontend.crt"
4041
)
4142

4243
// backendAuthority creates a volume projection of the PostgreSQL server
@@ -46,11 +47,20 @@ func backendAuthority(postgres *corev1.SecretProjection) corev1.VolumeProjection
4647
result := postgres.DeepCopy()
4748

4849
for i := range result.Items {
49-
if result.Items[i].Path == certBackendAuthorityProjectionPath {
50+
// The PostgreSQL server projection expects Path to match typical Keys.
51+
if result.Items[i].Path == tlsAuthoritySecretKey {
52+
result.Items[i].Path = certBackendAuthorityProjectionPath
5053
items = append(items, result.Items[i])
5154
}
5255
}
5356

57+
if len(items) == 0 {
58+
items = []corev1.KeyToPath{{
59+
Key: tlsAuthoritySecretKey,
60+
Path: certBackendAuthorityProjectionPath,
61+
}}
62+
}
63+
5464
result.Items = items
5565
return corev1.VolumeProjection{Secret: result}
5666
}
@@ -59,10 +69,8 @@ func backendAuthority(postgres *corev1.SecretProjection) corev1.VolumeProjection
5969
func frontendCertificate(
6070
custom *corev1.SecretProjection, secret *corev1.Secret,
6171
) corev1.VolumeProjection {
62-
result := custom
63-
64-
if result == nil {
65-
result = &corev1.SecretProjection{
72+
if custom == nil {
73+
return corev1.VolumeProjection{Secret: &corev1.SecretProjection{
6674
LocalObjectReference: corev1.LocalObjectReference{
6775
Name: secret.Name,
6876
},
@@ -80,8 +88,53 @@ func frontendCertificate(
8088
Path: certFrontendProjectionPath,
8189
},
8290
},
91+
}}
92+
}
93+
94+
// The custom projection may have more or less than the three items we need
95+
// to mount. Search for items that have the Path we expect and mount them at
96+
// the path we need. When no items are specified, the Key serves as the Path.
97+
98+
// TODO(cbandy): A more structured field or validating webhook would ensure
99+
// that the necessary values are specified.
100+
101+
var items []corev1.KeyToPath
102+
result := custom.DeepCopy()
103+
104+
for i := range result.Items {
105+
// The custom projection expects Path to match typical Keys.
106+
switch result.Items[i].Path {
107+
case tlsAuthoritySecretKey:
108+
result.Items[i].Path = certFrontendAuthorityProjectionPath
109+
items = append(items, result.Items[i])
110+
111+
case tlsCertificateSecretKey:
112+
result.Items[i].Path = certFrontendProjectionPath
113+
items = append(items, result.Items[i])
114+
115+
case tlsPrivateKeySecretKey:
116+
result.Items[i].Path = certFrontendPrivateKeyProjectionPath
117+
items = append(items, result.Items[i])
118+
}
119+
}
120+
121+
if len(items) == 0 {
122+
items = []corev1.KeyToPath{
123+
{
124+
Key: tlsAuthoritySecretKey,
125+
Path: certFrontendAuthorityProjectionPath,
126+
},
127+
{
128+
Key: tlsPrivateKeySecretKey,
129+
Path: certFrontendPrivateKeyProjectionPath,
130+
},
131+
{
132+
Key: tlsCertificateSecretKey,
133+
Path: certFrontendProjectionPath,
134+
},
83135
}
84136
}
85137

138+
result.Items = items
86139
return corev1.VolumeProjection{Secret: result}
87140
}

internal/pgbouncer/certificates_test.go

Lines changed: 47 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,62 +16,91 @@
1616
package pgbouncer
1717

1818
import (
19-
"strings"
2019
"testing"
2120

2221
"gotest.tools/v3/assert"
2322
corev1 "k8s.io/api/core/v1"
2423
)
2524

2625
func TestBackendAuthority(t *testing.T) {
26+
// No items; assume Key matches Path.
2727
projection := &corev1.SecretProjection{
2828
LocalObjectReference: corev1.LocalObjectReference{Name: "some-name"},
29-
Items: []corev1.KeyToPath{
30-
{Key: "some-crt-key", Path: "tls.crt"},
31-
{Key: "some-ca-key", Path: "ca.crt"},
32-
},
3329
}
30+
assert.Assert(t, marshalMatches(backendAuthority(projection), `
31+
secret:
32+
items:
33+
- key: ca.crt
34+
path: ~postgres-operator/backend-ca.crt
35+
name: some-name
36+
`))
3437

35-
assert.Assert(t, marshalEquals(backendAuthority(projection), strings.Trim(`
38+
// Some items; use only the CA Path.
39+
projection.Items = []corev1.KeyToPath{
40+
{Key: "some-crt-key", Path: "tls.crt"},
41+
{Key: "some-ca-key", Path: "ca.crt"},
42+
}
43+
assert.Assert(t, marshalMatches(backendAuthority(projection), `
3644
secret:
3745
items:
3846
- key: some-ca-key
39-
path: ca.crt
47+
path: ~postgres-operator/backend-ca.crt
4048
name: some-name
41-
`, "\t\n")+"\n"))
49+
`))
4250
}
4351

4452
func TestFrontendCertificate(t *testing.T) {
4553
secret := new(corev1.Secret)
4654
secret.Name = "op-secret"
4755

4856
t.Run("Generated", func(t *testing.T) {
49-
assert.Assert(t, marshalEquals(frontendCertificate(nil, secret), strings.Trim(`
57+
assert.Assert(t, marshalMatches(frontendCertificate(nil, secret), `
5058
secret:
5159
items:
5260
- key: pgbouncer-frontend.ca-roots
53-
path: ca.crt
61+
path: ~postgres-operator/frontend-ca.crt
5462
- key: pgbouncer-frontend.key
55-
path: tls.key
63+
path: ~postgres-operator/frontend-tls.key
5664
- key: pgbouncer-frontend.crt
57-
path: tls.crt
65+
path: ~postgres-operator/frontend-tls.crt
5866
name: op-secret
59-
`, "\t\n")+"\n"))
67+
`))
6068
})
6169

6270
t.Run("Custom", func(t *testing.T) {
6371
custom := new(corev1.SecretProjection)
6472
custom.Name = "some-other"
73+
74+
// No items; assume Key matches Path.
75+
assert.Assert(t, marshalMatches(frontendCertificate(custom, secret), `
76+
secret:
77+
items:
78+
- key: ca.crt
79+
path: ~postgres-operator/frontend-ca.crt
80+
- key: tls.key
81+
path: ~postgres-operator/frontend-tls.key
82+
- key: tls.crt
83+
path: ~postgres-operator/frontend-tls.crt
84+
name: some-other
85+
`))
86+
87+
// Some items; use only the TLS Paths.
6588
custom.Items = []corev1.KeyToPath{
6689
{Key: "any", Path: "thing"},
90+
{Key: "some-ca-key", Path: "ca.crt"},
91+
{Key: "some-cert-key", Path: "tls.crt"},
92+
{Key: "some-key-key", Path: "tls.key"},
6793
}
68-
69-
assert.Assert(t, marshalEquals(frontendCertificate(custom, secret), strings.Trim(`
94+
assert.Assert(t, marshalMatches(frontendCertificate(custom, secret), `
7095
secret:
7196
items:
72-
- key: any
73-
path: thing
97+
- key: some-ca-key
98+
path: ~postgres-operator/frontend-ca.crt
99+
- key: some-cert-key
100+
path: ~postgres-operator/frontend-tls.crt
101+
- key: some-key-key
102+
path: ~postgres-operator/frontend-tls.key
74103
name: some-other
75-
`, "\t\n")+"\n"))
104+
`))
76105
})
77106
}

internal/pgbouncer/config_test.go

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -77,15 +77,15 @@ verbose = 0
7777
auth_file = /etc/pgbouncer/~postgres-operator/users.txt
7878
auth_query = SELECT username, password from pgbouncer.get_auth($1)
7979
auth_user = _crunchypgbouncer
80-
client_tls_ca_file = /etc/pgbouncer/~postgres-operator-frontend/ca.crt
81-
client_tls_cert_file = /etc/pgbouncer/~postgres-operator-frontend/tls.crt
82-
client_tls_key_file = /etc/pgbouncer/~postgres-operator-frontend/tls.key
80+
client_tls_ca_file = /etc/pgbouncer/~postgres-operator/frontend-ca.crt
81+
client_tls_cert_file = /etc/pgbouncer/~postgres-operator/frontend-tls.crt
82+
client_tls_key_file = /etc/pgbouncer/~postgres-operator/frontend-tls.key
8383
client_tls_sslmode = require
8484
conffile = /etc/pgbouncer/~postgres-operator.ini
8585
ignore_startup_parameters = extra_float_digits
8686
listen_addr = *
8787
listen_port = 8888
88-
server_tls_ca_file = /etc/pgbouncer/~postgres-operator-backend/ca.crt
88+
server_tls_ca_file = /etc/pgbouncer/~postgres-operator/backend-ca.crt
8989
server_tls_sslmode = verify-full
9090
unix_socket_dir =
9191
@@ -120,15 +120,15 @@ verbose = whomp
120120
auth_file = /etc/pgbouncer/~postgres-operator/users.txt
121121
auth_query = SELECT username, password from pgbouncer.get_auth($1)
122122
auth_user = _crunchypgbouncer
123-
client_tls_ca_file = /etc/pgbouncer/~postgres-operator-frontend/ca.crt
124-
client_tls_cert_file = /etc/pgbouncer/~postgres-operator-frontend/tls.crt
125-
client_tls_key_file = /etc/pgbouncer/~postgres-operator-frontend/tls.key
123+
client_tls_ca_file = /etc/pgbouncer/~postgres-operator/frontend-ca.crt
124+
client_tls_cert_file = /etc/pgbouncer/~postgres-operator/frontend-tls.crt
125+
client_tls_key_file = /etc/pgbouncer/~postgres-operator/frontend-tls.key
126126
client_tls_sslmode = require
127127
conffile = /etc/pgbouncer/~postgres-operator.ini
128128
ignore_startup_parameters = custom
129129
listen_addr = *
130130
listen_port = 8888
131-
server_tls_ca_file = /etc/pgbouncer/~postgres-operator-backend/ca.crt
131+
server_tls_ca_file = /etc/pgbouncer/~postgres-operator/backend-ca.crt
132132
server_tls_sslmode = verify-full
133133
unix_socket_dir =
134134
@@ -154,7 +154,7 @@ func TestPodConfigFiles(t *testing.T) {
154154

155155
t.Run("Default", func(t *testing.T) {
156156
projections := podConfigFiles(config, configmap, secret)
157-
assert.Assert(t, marshalEquals(projections, strings.Trim(`
157+
assert.Assert(t, marshalMatches(projections, `
158158
- configMap:
159159
items:
160160
- key: pgbouncer-empty
@@ -170,7 +170,7 @@ func TestPodConfigFiles(t *testing.T) {
170170
- key: pgbouncer-users.txt
171171
path: ~postgres-operator/users.txt
172172
name: some-shh
173-
`, "\t\n")+"\n"))
173+
`))
174174
})
175175

176176
t.Run("CustomFiles", func(t *testing.T) {
@@ -187,7 +187,7 @@ func TestPodConfigFiles(t *testing.T) {
187187
}
188188

189189
projections := podConfigFiles(config, configmap, secret)
190-
assert.Assert(t, marshalEquals(projections, strings.Trim(`
190+
assert.Assert(t, marshalMatches(projections, `
191191
- configMap:
192192
items:
193193
- key: pgbouncer-empty
@@ -210,7 +210,7 @@ func TestPodConfigFiles(t *testing.T) {
210210
- key: pgbouncer-users.txt
211211
path: ~postgres-operator/users.txt
212212
name: some-shh
213-
`, "\t\n")+"\n"))
213+
`))
214214
})
215215
}
216216

0 commit comments

Comments
 (0)