forked from CrunchyData/postgres-operator
-
Notifications
You must be signed in to change notification settings - Fork 0
/
config.go
268 lines (227 loc) · 9.12 KB
/
config.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
/*
Copyright 2021 - 2022 Crunchy Data Solutions, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package pgbouncer
import (
"fmt"
"sort"
"strings"
corev1 "k8s.io/api/core/v1"
"github.com/adifri/postgres-operator/v5/internal/naming"
"github.com/adifri/postgres-operator/v5/pkg/apis/postgres-operator.crunchydata.com/v1beta1"
)
const (
configDirectory = "/etc/pgbouncer"
authFileAbsolutePath = configDirectory + "/" + authFileProjectionPath
emptyFileAbsolutePath = configDirectory + "/" + emptyFileProjectionPath
iniFileAbsolutePath = configDirectory + "/" + iniFileProjectionPath
authFileProjectionPath = "~postgres-operator/users.txt"
emptyFileProjectionPath = "pgbouncer.ini"
iniFileProjectionPath = "~postgres-operator.ini"
authFileSecretKey = "pgbouncer-users.txt" // #nosec G101 this is a name, not a credential
passwordSecretKey = "pgbouncer-password" // #nosec G101 this is a name, not a credential
verifierSecretKey = "pgbouncer-verifier" // #nosec G101 this is a name, not a credential
emptyConfigMapKey = "pgbouncer-empty"
iniFileConfigMapKey = "pgbouncer.ini"
)
const (
iniGeneratedWarning = "" +
"# Generated by postgres-operator. DO NOT EDIT.\n" +
"# Your changes will not be saved.\n"
)
type iniValueSet map[string]string
func (vs iniValueSet) String() string {
keys := make([]string, 0, len(vs))
for k := range vs {
keys = append(keys, k)
}
sort.Strings(keys)
var b strings.Builder
for _, k := range keys {
if len(vs[k]) <= 0 {
_, _ = fmt.Fprintf(&b, "%s =\n", k)
} else {
_, _ = fmt.Fprintf(&b, "%s = %s\n", k, vs[k])
}
}
return b.String()
}
// authFileContents returns a PgBouncer user database.
func authFileContents(password string) []byte {
// > There should be at least 2 fields, surrounded by double quotes.
// > Double quotes in a field value can be escaped by writing two double quotes.
// - https://www.pgbouncer.org/config.html#authentication-file-format
quote := func(s string) string {
return `"` + strings.ReplaceAll(s, `"`, `""`) + `"`
}
user1 := quote(postgresqlUser) + " " + quote(password) + "\n"
return []byte(user1)
}
func clusterINI(cluster *v1beta1.PostgresCluster) string {
var (
pgBouncerPort = *cluster.Spec.Proxy.PGBouncer.Port
postgresPort = *cluster.Spec.Port
)
global := iniValueSet{
// Prior to PostgreSQL v12, the default setting for "extra_float_digits"
// does not return precise float values. Applications that want
// consistent results from different PostgreSQL versions may connect
// with this startup parameter. The JDBC driver uses it regardless.
// Trust that applications that know or care about this setting are
// using it consistently within each connection pool.
// - https://www.postgresql.org/docs/current/runtime-config-client.html#GUC-EXTRA-FLOAT-DIGITS
// - https://github.com/pgjdbc/pgjdbc/blob/REL42.2.19/pgjdbc/src/main/java/org/postgresql/core/v3/ConnectionFactoryImpl.java#L334
"ignore_startup_parameters": "extra_float_digits",
// Authenticate frontend connections using passwords stored in PostgreSQL.
// PgBouncer will connect to the backend database that is requested by
// the frontend as the "auth_user" and execute "auth_query". When
// "auth_user" requires a password, PgBouncer reads it from "auth_file".
"auth_file": authFileAbsolutePath,
"auth_query": "SELECT username, password from pgbouncer.get_auth($1)",
"auth_user": postgresqlUser,
// TODO(cbandy): Use an HBA file to control authentication of PgBouncer
// accounts; e.g. "admin_users" below.
// - https://www.pgbouncer.org/config.html#hba-file-format
//"auth_hba_file": "",
//"auth_type": "hba",
//"admin_users": "pgbouncer",
// Require TLS encryption on client connections.
"client_tls_sslmode": "require",
"client_tls_cert_file": certFrontendAbsolutePath,
"client_tls_key_file": certFrontendPrivateKeyAbsolutePath,
"client_tls_ca_file": certFrontendAuthorityAbsolutePath,
// Listen on the PgBouncer port on all addresses.
"listen_addr": "*",
"listen_port": fmt.Sprint(pgBouncerPort),
// Require TLS encryption on connections to PostgreSQL.
"server_tls_sslmode": "verify-full",
"server_tls_ca_file": certBackendAuthorityAbsolutePath,
// Disable Unix sockets to keep the filesystem read-only.
"unix_socket_dir": "",
}
// Override the above with any specified settings.
for k, v := range cluster.Spec.Proxy.PGBouncer.Config.Global {
global[k] = v
}
// Prevent the user from bypassing the main configuration file.
global["conffile"] = iniFileAbsolutePath
// Use a wildcard to automatically create connection pools based on database
// names. These pools connect to cluster's primary service. The service name
// is an RFC 1123 DNS label so it does not need to be quoted nor escaped.
// - https://www.pgbouncer.org/config.html#section-databases
//
// NOTE(cbandy): PgBouncer only accepts connections to items in this section
// and the database "pgbouncer", which is the admin console. For connections
// to the wildcard, PgBouncer first checks for the database in PostgreSQL.
// When that database does not exist, the client will experience timeouts
// or errors that sound like PgBouncer misconfiguration.
// - https://github.com/pgbouncer/pgbouncer/issues/352
databases := iniValueSet{
"*": fmt.Sprintf("host=%s port=%d",
naming.ClusterPrimaryService(cluster).Name, postgresPort),
}
// Replace the above with any specified databases.
if len(cluster.Spec.Proxy.PGBouncer.Config.Databases) > 0 {
databases = iniValueSet(cluster.Spec.Proxy.PGBouncer.Config.Databases)
}
users := iniValueSet(cluster.Spec.Proxy.PGBouncer.Config.Users)
// Include any custom configuration file, then apply global settings, then
// pool definitions.
result := iniGeneratedWarning +
"\n[pgbouncer]" +
"\n%include " + emptyFileAbsolutePath +
"\n\n[pgbouncer]\n" + global.String() +
"\n[databases]\n" + databases.String()
if len(users) > 0 {
result += "\n[users]\n" + users.String()
}
return result
}
// podConfigFiles returns projections of PgBouncer's configuration files to
// include in the configuration volume.
func podConfigFiles(
config v1beta1.PGBouncerConfiguration,
configmap *corev1.ConfigMap, secret *corev1.Secret,
) []corev1.VolumeProjection {
// Start with an empty file at /etc/pgbouncer/pgbouncer.ini. This file can
// be overridden by the user, but it must exist because our configuration
// file refers to it.
projections := []corev1.VolumeProjection{
{
ConfigMap: &corev1.ConfigMapProjection{
LocalObjectReference: corev1.LocalObjectReference{
Name: configmap.Name,
},
Items: []corev1.KeyToPath{{
Key: emptyConfigMapKey,
Path: emptyFileProjectionPath,
}},
},
},
}
// Add any specified projections. These may override the files above.
// - https://docs.k8s.io/concepts/storage/volumes/#projected
projections = append(projections, config.Files...)
// Add our non-empty configurations last so that they take precedence.
projections = append(projections, []corev1.VolumeProjection{
{
ConfigMap: &corev1.ConfigMapProjection{
LocalObjectReference: corev1.LocalObjectReference{
Name: configmap.Name,
},
Items: []corev1.KeyToPath{{
Key: iniFileConfigMapKey,
Path: iniFileProjectionPath,
}},
},
},
{
Secret: &corev1.SecretProjection{
LocalObjectReference: corev1.LocalObjectReference{
Name: secret.Name,
},
Items: []corev1.KeyToPath{{
Key: authFileSecretKey,
Path: authFileProjectionPath,
}},
},
},
}...)
return projections
}
// reloadCommand returns an entrypoint that convinces PgBouncer to reload
// configuration files. The process will appear as name in `ps` and `top`.
func reloadCommand(name string) []string {
// Use a Bash loop to periodically check the mtime of the mounted
// configuration volume. When it changes, signal PgBouncer and print the
// observed timestamp.
//
// Coreutils `sleep` uses a lot of memory, so the following opens a file
// descriptor and uses the timeout of the builtin `read` to wait. That same
// descriptor gets closed and reopened to use the builtin `[ -nt` to check
// mtimes.
// - https://unix.stackexchange.com/a/407383
const script = `
exec {fd}<> <(:)
while read -r -t 5 -u "${fd}" || true; do
if [ "${directory}" -nt "/proc/self/fd/${fd}" ] && pkill -HUP --exact pgbouncer
then
exec {fd}>&- && exec {fd}<> <(:)
stat --format='Loaded configuration dated %y' "${directory}"
fi
done
`
// Elide the above script from `ps` and `top` by wrapping it in a function
// and calling that.
wrapper := `monitor() {` + script + `}; export directory="$1"; export -f monitor; exec -a "$0" bash -ceu monitor`
return []string{"bash", "-ceu", "--", wrapper, name, configDirectory}
}