feat(pooler): render pgbouncer.ini, checksum, and config map
This commit is contained in:
parent
db689033a0
commit
3dabf6c379
|
|
@ -0,0 +1,169 @@
|
|||
package cluster
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
acidv1 "github.com/zalando/postgres-operator/pkg/apis/acid.zalan.do/v1"
|
||||
"github.com/zalando/postgres-operator/pkg/util"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
const (
|
||||
pgBouncerConfigFileName = "pgbouncer.ini"
|
||||
poolerConfigChecksumAnnotation = "postgres-operator.zalando.org/pgbouncer-config-checksum"
|
||||
)
|
||||
|
||||
// FIPS-friendly pgbouncer.ini. Differs from the stock Zalando image template:
|
||||
// no logfile/pidfile (distroless logs to stdout), auth_type is configurable,
|
||||
// and TLS cert lines are emitted only when spec.TLS is set (the FIPS image does
|
||||
// not run the openssl cert-generation step that the Zalando entrypoint does).
|
||||
const pgBouncerConfigTemplateText = `# Generated by postgres-operator. Do not edit.
|
||||
[databases]
|
||||
* = host={{ .DBHost }} port={{ .DBPort }} auth_user={{ .User }}
|
||||
postgres = host={{ .DBHost }} port={{ .DBPort }} auth_user={{ .User }}
|
||||
|
||||
[pgbouncer]
|
||||
pool_mode = {{ .Mode }}
|
||||
listen_port = {{ .ListenPort }}
|
||||
listen_addr = *
|
||||
admin_users = {{ .User }}
|
||||
{{- if .StatsUsers }}
|
||||
stats_users = {{ .StatsUsers }}
|
||||
{{- end }}
|
||||
auth_dbname = postgres
|
||||
auth_file = /etc/pgbouncer/userlist.txt
|
||||
auth_query = SELECT * FROM {{ .Schema }}.user_lookup($1)
|
||||
auth_type = {{ .AuthType }}
|
||||
server_tls_sslmode = require
|
||||
{{- if .TLS }}
|
||||
{{- if .TLSCAFile }}
|
||||
server_tls_ca_file = {{ .TLSCAFile }}
|
||||
{{- end }}
|
||||
client_tls_sslmode = require
|
||||
client_tls_key_file = {{ .TLSKeyFile }}
|
||||
client_tls_cert_file = {{ .TLSCertFile }}
|
||||
{{- end }}
|
||||
log_connections = 0
|
||||
log_disconnections = 0
|
||||
max_prepared_statements = 200
|
||||
default_pool_size = {{ .DefaultPoolSize }}
|
||||
reserve_pool_size = {{ .ReservePoolSize }}
|
||||
max_client_conn = {{ .MaxClientConn }}
|
||||
max_db_connections = {{ .MaxDBConnections }}
|
||||
idle_transaction_timeout = 600
|
||||
server_login_retry = 5
|
||||
ignore_startup_parameters = extra_float_digits,options
|
||||
`
|
||||
|
||||
var pgBouncerConfigTemplate = template.Must(
|
||||
template.New(pgBouncerConfigFileName).Parse(pgBouncerConfigTemplateText))
|
||||
|
||||
type pgBouncerConfigParams struct {
|
||||
DBHost string
|
||||
DBPort int32
|
||||
ListenPort int32
|
||||
User string
|
||||
Schema string
|
||||
Mode string
|
||||
AuthType string
|
||||
StatsUsers string
|
||||
DefaultPoolSize int32
|
||||
ReservePoolSize int32
|
||||
MaxClientConn int32
|
||||
MaxDBConnections int32
|
||||
TLS bool
|
||||
TLSCAFile string
|
||||
TLSKeyFile string
|
||||
TLSCertFile string
|
||||
}
|
||||
|
||||
// generatePgBouncerIni renders the pgbouncer.ini for the given role from the
|
||||
// cluster spec and operator config.
|
||||
func (c *Cluster) generatePgBouncerIni(role PostgresRole) (string, error) {
|
||||
spec := &c.Spec
|
||||
connectionPoolerSpec := spec.ConnectionPooler
|
||||
if connectionPoolerSpec == nil {
|
||||
connectionPoolerSpec = &acidv1.ConnectionPooler{}
|
||||
}
|
||||
|
||||
sizes := c.connectionPoolerSizes()
|
||||
|
||||
infraRolesList := make([]string, 0)
|
||||
for infraRoleName := range c.InfrastructureRoles {
|
||||
infraRolesList = append(infraRolesList, infraRoleName)
|
||||
}
|
||||
sort.Strings(infraRolesList) // deterministic output for stable checksums
|
||||
|
||||
params := pgBouncerConfigParams{
|
||||
DBHost: c.serviceAddress(role),
|
||||
DBPort: c.servicePort(role),
|
||||
ListenPort: pgPort,
|
||||
User: util.Coalesce(connectionPoolerSpec.User, c.OpConfig.ConnectionPooler.User),
|
||||
Schema: util.Coalesce(connectionPoolerSpec.Schema, c.OpConfig.ConnectionPooler.Schema),
|
||||
Mode: util.Coalesce(connectionPoolerSpec.Mode, c.OpConfig.ConnectionPooler.Mode),
|
||||
AuthType: c.OpConfig.ConnectionPooler.AuthType,
|
||||
StatsUsers: strings.Join(infraRolesList, ","),
|
||||
DefaultPoolSize: sizes.defaultSize,
|
||||
ReservePoolSize: sizes.reserveSize,
|
||||
MaxClientConn: sizes.maxClientConn,
|
||||
MaxDBConnections: sizes.maxDBConn,
|
||||
}
|
||||
|
||||
if spec.TLS != nil && spec.TLS.SecretName != "" {
|
||||
mountPath := "/tls"
|
||||
params.TLS = true
|
||||
params.TLSCertFile = ensurePath(spec.TLS.CertificateFile, mountPath, "tls.crt")
|
||||
params.TLSKeyFile = ensurePath(spec.TLS.PrivateKeyFile, mountPath, "tls.key")
|
||||
if spec.TLS.CAFile != "" {
|
||||
mountPathCA := mountPath
|
||||
if spec.TLS.CASecretName != "" {
|
||||
mountPathCA = mountPath + "ca"
|
||||
}
|
||||
params.TLSCAFile = ensurePath(spec.TLS.CAFile, mountPathCA, "")
|
||||
}
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := pgBouncerConfigTemplate.Execute(&buf, params); err != nil {
|
||||
return "", fmt.Errorf("could not render pgbouncer config: %v", err)
|
||||
}
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
// connectionPoolerConfigChecksum returns the sha256 of the rendered config,
|
||||
// used as a pod annotation so config changes roll the pooler pods.
|
||||
func (c *Cluster) connectionPoolerConfigChecksum(role PostgresRole) (string, error) {
|
||||
ini, err := c.generatePgBouncerIni(role)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
sum := sha256.Sum256([]byte(ini))
|
||||
return fmt.Sprintf("%x", sum), nil
|
||||
}
|
||||
|
||||
// generateConnectionPoolerConfigMap builds the operator-owned ConfigMap holding
|
||||
// the rendered pgbouncer.ini for the given role.
|
||||
func (c *Cluster) generateConnectionPoolerConfigMap(role PostgresRole) (*v1.ConfigMap, error) {
|
||||
ini, err := c.generatePgBouncerIni(role)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &v1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: fmt.Sprintf("%s-config", c.connectionPoolerName(role)),
|
||||
Namespace: c.Namespace,
|
||||
Labels: c.connectionPoolerLabels(role, true).MatchLabels,
|
||||
Annotations: c.annotationsSet(nil),
|
||||
OwnerReferences: c.ownerReferences(),
|
||||
},
|
||||
Data: map[string]string{
|
||||
pgBouncerConfigFileName: ini,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
package cluster
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
acidv1 "github.com/zalando/postgres-operator/pkg/apis/acid.zalan.do/v1"
|
||||
"github.com/zalando/postgres-operator/pkg/util"
|
||||
"github.com/zalando/postgres-operator/pkg/util/config"
|
||||
"github.com/zalando/postgres-operator/pkg/util/k8sutil"
|
||||
)
|
||||
|
||||
func newGenerateConfigCluster() *Cluster {
|
||||
maxDB := int32(60)
|
||||
instances := int32(2)
|
||||
cluster := New(
|
||||
Config{OpConfig: config.Config{
|
||||
ConnectionPooler: config.ConnectionPooler{
|
||||
User: "pooler",
|
||||
Schema: "pooler",
|
||||
Mode: "transaction",
|
||||
MaxDBConnections: &maxDB,
|
||||
NumberOfInstances: &instances,
|
||||
GenerateConfig: true,
|
||||
AuthType: "scram-sha-256",
|
||||
ConfigPath: "/etc/pgbouncer/pgbouncer.ini",
|
||||
Args: []string{"/etc/pgbouncer/pgbouncer.ini"},
|
||||
},
|
||||
Resources: config.Resources{
|
||||
EnableOwnerReferences: util.True(),
|
||||
},
|
||||
}},
|
||||
k8sutil.NewMockKubernetesClient(), acidv1.Postgresql{}, logger, eventRecorder)
|
||||
cluster.Name = "acid-test"
|
||||
cluster.Namespace = "default"
|
||||
cluster.Spec = acidv1.PostgresSpec{ConnectionPooler: &acidv1.ConnectionPooler{}}
|
||||
return cluster
|
||||
}
|
||||
|
||||
func TestGeneratePgBouncerIni(t *testing.T) {
|
||||
cluster := newGenerateConfigCluster()
|
||||
|
||||
ini, err := cluster.generatePgBouncerIni(Master)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
for _, want := range []string{
|
||||
"[databases]",
|
||||
"[pgbouncer]",
|
||||
"pool_mode = transaction",
|
||||
"auth_type = scram-sha-256",
|
||||
"auth_file = /etc/pgbouncer/userlist.txt",
|
||||
"auth_query = SELECT * FROM pooler.user_lookup($1)",
|
||||
"server_tls_sslmode = require",
|
||||
"default_pool_size = 15",
|
||||
"max_db_connections = 30",
|
||||
} {
|
||||
if !strings.Contains(ini, want) {
|
||||
t.Errorf("rendered ini missing %q\n---\n%s", want, ini)
|
||||
}
|
||||
}
|
||||
|
||||
if strings.Contains(ini, "client_tls_cert_file") {
|
||||
t.Errorf("did not expect client_tls_cert_file without spec.TLS\n%s", ini)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGeneratePgBouncerIniWithTLS(t *testing.T) {
|
||||
cluster := newGenerateConfigCluster()
|
||||
cluster.Spec.TLS = &acidv1.TLSDescription{SecretName: "pg-tls"}
|
||||
|
||||
ini, err := cluster.generatePgBouncerIni(Master)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
for _, want := range []string{
|
||||
"client_tls_sslmode = require",
|
||||
"client_tls_key_file = /tls/tls.key",
|
||||
"client_tls_cert_file = /tls/tls.crt",
|
||||
} {
|
||||
if !strings.Contains(ini, want) {
|
||||
t.Errorf("rendered ini missing %q\n---\n%s", want, ini)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnectionPoolerConfigChecksumStability(t *testing.T) {
|
||||
cluster := newGenerateConfigCluster()
|
||||
|
||||
sum1, err := cluster.connectionPoolerConfigChecksum(Master)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
sum2, err := cluster.connectionPoolerConfigChecksum(Master)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if sum1 != sum2 {
|
||||
t.Errorf("checksum not stable: %q != %q", sum1, sum2)
|
||||
}
|
||||
|
||||
cluster.OpConfig.ConnectionPooler.AuthType = "md5"
|
||||
sum3, err := cluster.connectionPoolerConfigChecksum(Master)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if sum1 == sum3 {
|
||||
t.Errorf("checksum should change when config changes")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateConnectionPoolerConfigMap(t *testing.T) {
|
||||
cluster := newGenerateConfigCluster()
|
||||
|
||||
cm, err := cluster.generateConnectionPoolerConfigMap(Master)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if cm.Name != cluster.connectionPoolerName(Master)+"-config" {
|
||||
t.Errorf("unexpected config map name %q", cm.Name)
|
||||
}
|
||||
if _, ok := cm.Data["pgbouncer.ini"]; !ok {
|
||||
t.Errorf("config map missing pgbouncer.ini key, got %#v", cm.Data)
|
||||
}
|
||||
if len(cm.OwnerReferences) == 0 {
|
||||
t.Errorf("config map should have owner references")
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue