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 } // 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 }