193 lines
6.2 KiB
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
|
|
}
|