postgres-operator/pkg/cluster/pgbouncer_config.go

193 lines
6.2 KiB
Go

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 = "acid.zalan.do/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
}
// connectionPoolerConfigMapName returns the name of the operator-generated
// pgbouncer config map for the given role.
func (c *Cluster) connectionPoolerConfigMapName(role PostgresRole) string {
return fmt.Sprintf("%s-config", c.connectionPoolerName(role))
}
// 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: c.connectionPoolerConfigMapName(role),
Namespace: c.Namespace,
Labels: c.connectionPoolerLabels(role, true).MatchLabels,
Annotations: c.annotationsSet(nil),
OwnerReferences: c.ownerReferences(),
},
Data: map[string]string{
pgBouncerConfigFileName: ini,
},
}, nil
}
// connectionPoolerPodAnnotations returns the pooler pod annotations, adding the
// config checksum when generated config is enabled so config changes roll pods.
func (c *Cluster) connectionPoolerPodAnnotations(role PostgresRole) (map[string]string, error) {
annotations := c.annotationsSet(c.generatePodAnnotations(&c.Spec))
if c.OpConfig.ConnectionPooler.GenerateConfig {
checksum, err := c.connectionPoolerConfigChecksum(role)
if err != nil {
return nil, err
}
if annotations == nil {
annotations = map[string]string{}
}
annotations[poolerConfigChecksumAnnotation] = checksum
}
return annotations, nil
}