active_postgres 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0266b3f7dabc2d844b30aa6410cfd7c59131a7fb77a2dea85f01f9a66c2a2443
4
- data.tar.gz: fd720306229f1ddea1d4e01a9dc3a5df3b7a60a620c38e86779e82f9698ebcbc
3
+ metadata.gz: 967673143f28952b4f44c54cb97282ca9ca288fe8885fc525433baac00633db0
4
+ data.tar.gz: a43859d367af55c17c9a6a407a39e2e492cc829806ca615dd4f82b03604303f0
5
5
  SHA512:
6
- metadata.gz: bb3bc17966f9ebe98a55f09f678fe2bc2e57b92615c45848c347a7568f6e3bb61d15788d6bb16008e4498a9b944a3f53241f2e2579b204d28679341b041f4978
7
- data.tar.gz: 3863cace22d1ba3aea901d312d26152de067c7ca898ddff5b654a75702a9e6b4c01f6ffbac820fed338fc5db8ce011ad39676143f81f785f66f7fec72cd72ae5
6
+ metadata.gz: f6dc9378f1391e20051028100f9767a65a9de62642719b4a413605a9208fa9a25b5e0c6790c9fdd0e966080293e5f33e9a5b9e860c5622cc20b65d933d70dae6
7
+ data.tar.gz: 17b10e8adba8c2a502d0217f156ce68e7c2d1f4e4442fddd82b3ad8e560a940dc7a07a360b9a662dd8fbf0e8f80af7aff2f37bc6d9aaba7c0195b8590c32fbb3
data/README.md CHANGED
@@ -7,7 +7,7 @@ Production-grade PostgreSQL HA for Rails.
7
7
  - **High Availability**: Primary/standby replication with automatic failover (repmgr)
8
8
  - **Connection Pooling**: PgBouncer integration
9
9
  - **Rails Integration**: Automatic database.yml config, migration guard, read replica routing
10
- - **Modular Components**: Core, Performance Tuning, repmgr, PgBouncer, pgBackRest, Monitoring, SSL, Extensions
10
+ - **Modular Components**: Core, Performance Tuning (opt-in), repmgr, PgBouncer, pgBackRest, Monitoring, SSL, Extensions
11
11
 
12
12
  ## Quick Start
13
13
 
@@ -48,6 +48,7 @@ production:
48
48
  superuser_password: $POSTGRES_SUPERUSER_PASSWORD
49
49
  replication_password: $POSTGRES_REPLICATION_PASSWORD
50
50
  repmgr_password: $POSTGRES_REPMGR_PASSWORD
51
+ monitoring_password: $POSTGRES_MONITORING_PASSWORD
51
52
  app_password: $POSTGRES_APP_PASSWORD
52
53
  ```
53
54
 
@@ -110,14 +111,84 @@ active_postgres cache-secrets
110
111
  | Component | Description | Config |
111
112
  |-----------|-------------|--------|
112
113
  | **Core** | PostgreSQL installation | Always enabled |
113
- | **Performance Tuning** | Auto-optimization | Enabled by default |
114
+ | **Performance Tuning** | Auto-optimization | Disabled by default |
114
115
  | **repmgr** | HA & automatic failover | `repmgr: {enabled: true}` |
115
116
  | **PgBouncer** | Connection pooling | `pgbouncer: {enabled: true}` |
116
117
  | **pgBackRest** | Backup & restore | `pgbackrest: {enabled: true}` |
117
- | **Monitoring** | postgres_exporter | `monitoring: {enabled: true}` |
118
+ | **Monitoring** | postgres_exporter (configures a dedicated pg_monitor user) | `monitoring: {enabled: true}` |
118
119
  | **SSL** | Encrypted connections | `ssl: {enabled: true}` |
119
120
  | **Extensions** | pgvector, PostGIS, etc. | `extensions: {enabled: true, list: [pgvector]}` |
120
121
 
122
+ ### Monitoring credentials
123
+
124
+ `postgres_exporter` uses a dedicated `pg_monitor` user. Provide a password in secrets and optionally set the username:
125
+
126
+ ```yaml
127
+ components:
128
+ monitoring: {enabled: true, user: postgres_exporter}
129
+ secrets:
130
+ monitoring_password: $POSTGRES_MONITORING_PASSWORD
131
+ ```
132
+
133
+ ### pgBackRest S3-compatible endpoints
134
+
135
+ For Tigris/MinIO/Wasabi/DO Spaces, set a custom endpoint and use path-style URLs:
136
+
137
+ ```yaml
138
+ components:
139
+ pgbackrest:
140
+ enabled: true
141
+ repo_type: s3
142
+ s3_bucket: myapp-backups
143
+ s3_region: auto
144
+ s3_endpoint: t3.storage.dev
145
+ s3_uri_style: path
146
+ ```
147
+
148
+ ### Stable app endpoint with PgBouncer
149
+
150
+ If you run PgBouncer on each PostgreSQL node and want a fixed app URL, enable `follow_primary`.
151
+ Each PgBouncer instance periodically repoints to the current primary using repmgr metadata.
152
+ Put a TCP load balancer or DNS record in front of the PgBouncer nodes.
153
+
154
+ ```yaml
155
+ components:
156
+ pgbouncer:
157
+ enabled: true
158
+ follow_primary: true
159
+ follow_primary_interval: 5
160
+ ```
161
+
162
+ ### Automatic DNS updates on failover (dnsmasq)
163
+
164
+ If you use Messhy’s mesh DNS (dnsmasq), repmgr can update writer/reader DNS records on failover.
165
+ Enable `dns_failover` under `repmgr` and provide DNS server IPs or hostnames.
166
+ If you run setup from outside the mesh, use objects with both the public SSH host
167
+ and the private WireGuard IP so setup can SSH in and failover updates stay on the mesh.
168
+ If you run setup from inside the mesh, you can use a simple list of private IPs.
169
+
170
+ ```yaml
171
+ components:
172
+ repmgr:
173
+ enabled: true
174
+ dns_failover:
175
+ enabled: true
176
+ provider: dnsmasq
177
+ domain: mesh.internal
178
+ dns_servers:
179
+ - host: 18.170.173.14
180
+ private_ip: 10.8.0.10
181
+ - host: 98.85.183.175
182
+ private_ip: 10.8.0.110
183
+ primary_record: db-primary.mesh.internal
184
+ replica_record: db-replica.mesh.internal
185
+ ```
186
+
187
+ This installs an event hook so repmgr updates `/etc/dnsmasq.d/active_postgres.conf`
188
+ on the DNS servers whenever the primary changes.
189
+ DNS servers must be reachable over the private network and allow SSH from the
190
+ database nodes (active_postgres installs SSH keys on first setup).
191
+
121
192
  ## Secrets Management
122
193
 
123
194
  ```yaml
@@ -127,7 +198,8 @@ secrets:
127
198
 
128
199
  # Command execution
129
200
  password: $(op read "op://vault/item/field")
130
- password: $(rails runner "puts Rails.application.credentials.dig(:postgres, :password)")
201
+ password: rails_credentials:postgres.password
202
+ password: credentials:postgres.password
131
203
 
132
204
  # AWS Secrets Manager
133
205
  password: $(aws secretsmanager get-secret-value --secret-id myapp/postgres --query SecretString)
@@ -150,9 +222,19 @@ User.all # → Replica
150
222
  - Ruby 3.0+
151
223
  - PostgreSQL 12+
152
224
  - Ubuntu 20.04+ / Debian 11+ with systemd
153
- - SSH key-based authentication
225
+ - SSH key-based authentication (hosts must be in `~/.ssh/known_hosts`)
154
226
  - Rails 6.0+ (optional)
155
227
 
228
+ ### SSH host key verification
229
+
230
+ By default, host keys are verified strictly (`always`). For first-time provisioning you can set:
231
+
232
+ ```yaml
233
+ ssh_host_key_verification: accept_new
234
+ ```
235
+
236
+ This trusts a host key the first time and then pins it; prefer `always` once hosts are known.
237
+
156
238
  ## License
157
239
 
158
240
  MIT
@@ -189,14 +189,9 @@ module ActivePostgres
189
189
  app_password = resolve_app_password
190
190
  sql = build_app_user_sql(app_user, app_database, app_password)
191
191
 
192
- ssh_executor.execute_on_host(host) do
193
- upload! StringIO.new(sql), '/tmp/create_app_user.sql'
194
- execute :chmod, '644', '/tmp/create_app_user.sql'
195
- execute :sudo, '-u', 'postgres', 'psql', '-f', '/tmp/create_app_user.sql'
196
- execute :rm, '-f', '/tmp/create_app_user.sql'
192
+ ssh_executor.run_sql(host, sql, postgres_user: 'postgres', tuples_only: false, capture: false)
197
193
 
198
- puts " ✓ Created app user '#{app_user}' and database '#{app_database}'"
199
- end
194
+ puts " ✓ Created app user '#{app_user}' and database '#{app_database}'"
200
195
  rescue StandardError => e
201
196
  warn " Warning: Could not create app user: #{e.message}"
202
197
  warn ' You may need to create the user manually'
@@ -33,17 +33,27 @@ module ActivePostgres
33
33
  puts 'Extensions uninstall not implemented (extensions remain in database)'
34
34
  end
35
35
 
36
- private
36
+ def restart
37
+ puts 'Extensions do not require restart (loaded at database connection time)'
38
+ end
37
39
 
38
- def install_on_primary(extensions)
39
- host = config.primary_host
40
- version = config.version
41
- db_name = config.secrets_config['database_name'] || 'postgres'
42
- postgres_user = config.postgres_user
40
+ def install_on_standby(standby_host)
41
+ extensions_config = config.component_config(:extensions)
42
+ return unless extensions_config[:enabled]
43
43
 
44
- puts " Installing extensions on primary (#{host})..."
44
+ extensions = extensions_config[:list] || []
45
+ return if extensions.empty?
46
+
47
+ puts "Installing extension packages on standby #{standby_host}..."
48
+ install_packages_on_host(standby_host, extensions)
49
+ end
50
+
51
+ private
45
52
 
53
+ def install_packages_on_host(host, extensions)
54
+ version = config.version
46
55
  packages_to_install = []
56
+
47
57
  extensions.each do |ext_name|
48
58
  package = EXTENSION_PACKAGES[ext_name]
49
59
  next unless package
@@ -52,46 +62,41 @@ module ActivePostgres
52
62
  packages_to_install << package
53
63
  end
54
64
 
65
+ return if packages_to_install.empty?
66
+
55
67
  ssh_executor.execute_on_host(host) do
56
- unless packages_to_install.empty?
57
- execute :sudo, 'apt-get', 'update', '-qq'
58
- execute :sudo, 'DEBIAN_FRONTEND=noninteractive', 'apt-get', 'install', '-y', '-qq',
59
- *packages_to_install
60
- end
68
+ execute :sudo, 'apt-get', 'update', '-qq'
69
+ execute :sudo, 'DEBIAN_FRONTEND=noninteractive', 'apt-get', 'install', '-y', '-qq',
70
+ *packages_to_install
71
+ end
72
+ end
73
+
74
+ def install_on_primary(extensions)
75
+ host = config.primary_host
76
+ db_name = config.secrets_config['database_name'] || 'postgres'
77
+ postgres_user = config.postgres_user
78
+
79
+ puts " Installing extensions on primary (#{host})..."
80
+
81
+ install_packages_on_host(host, extensions)
61
82
 
83
+ ssh_executor.execute_on_host(host) do
62
84
  extensions.each do |ext_name|
63
- sql = "CREATE EXTENSION IF NOT EXISTS #{ext_name};"
85
+ sql = "CREATE EXTENSION IF NOT EXISTS \"#{ext_name}\";"
64
86
  begin
65
87
  execute :sudo, '-u', postgres_user, 'psql', '-d', db_name, '-c', sql
66
- rescue StandardError
67
- nil
88
+ info "✓ Extension #{ext_name} created/verified"
89
+ rescue StandardError => e
90
+ warn "⚠ Could not create extension #{ext_name}: #{e.message}"
68
91
  end
69
92
  end
70
93
  end
71
94
  end
72
95
 
73
96
  def install_on_standbys(extensions)
74
- version = config.version
75
-
76
97
  config.standby_hosts.each do |host|
77
- puts " Installing extensions on standby (#{host})..."
78
-
79
- packages_to_install = []
80
- extensions.each do |ext_name|
81
- package = EXTENSION_PACKAGES[ext_name]
82
- next unless package
83
-
84
- package = package.gsub('{version}', version.to_s)
85
- packages_to_install << package
86
- end
87
-
88
- ssh_executor.execute_on_host(host) do
89
- unless packages_to_install.empty?
90
- execute :sudo, 'apt-get', 'update', '-qq'
91
- execute :sudo, 'DEBIAN_FRONTEND=noninteractive', 'apt-get', 'install', '-y', '-qq',
92
- *packages_to_install
93
- end
94
- end
98
+ puts " Installing extension packages on standby (#{host})..."
99
+ install_packages_on_host(host, extensions)
95
100
  end
96
101
  end
97
102
  end
@@ -1,9 +1,13 @@
1
+ require 'cgi'
2
+
1
3
  module ActivePostgres
2
4
  module Components
3
5
  class Monitoring < Base
4
6
  def install
5
7
  puts 'Installing postgres_exporter for monitoring...'
6
8
 
9
+ ensure_monitoring_user
10
+
7
11
  config.all_hosts.each do |host|
8
12
  install_on_host(host)
9
13
  end
@@ -14,8 +18,9 @@ module ActivePostgres
14
18
 
15
19
  config.all_hosts.each do |host|
16
20
  ssh_executor.execute_on_host(host) do
17
- execute :sudo, 'systemctl', 'stop', 'postgres_exporter'
18
- execute :sudo, 'systemctl', 'disable', 'postgres_exporter'
21
+ execute :sudo, 'systemctl', 'stop', 'prometheus-postgres-exporter'
22
+ execute :sudo, 'systemctl', 'disable', 'prometheus-postgres-exporter'
23
+ execute :sudo, 'apt-get', 'remove', '-y', 'prometheus-postgres-exporter'
19
24
  end
20
25
  end
21
26
  end
@@ -25,11 +30,16 @@ module ActivePostgres
25
30
 
26
31
  config.all_hosts.each do |host|
27
32
  ssh_executor.execute_on_host(host) do
28
- execute :sudo, 'systemctl', 'restart', 'postgres_exporter'
33
+ execute :sudo, 'systemctl', 'restart', 'prometheus-postgres-exporter'
29
34
  end
30
35
  end
31
36
  end
32
37
 
38
+ def install_on_standby(standby_host)
39
+ puts "Installing postgres_exporter on standby #{standby_host}..."
40
+ install_on_host(standby_host)
41
+ end
42
+
33
43
  private
34
44
 
35
45
  def install_on_host(host)
@@ -42,14 +52,91 @@ module ActivePostgres
42
52
  ssh_executor.execute_on_host(host) do
43
53
  # Install via package manager or download binary
44
54
  execute :sudo, 'apt-get', 'install', '-y', '-qq', 'prometheus-postgres-exporter'
55
+ end
56
+
57
+ configure_exporter_service(host, monitoring_config, exporter_port)
45
58
 
59
+ ssh_executor.execute_on_host(host) do
46
60
  # Enable and start
47
61
  execute :sudo, 'systemctl', 'enable', 'prometheus-postgres-exporter'
48
- execute :sudo, 'systemctl', 'start', 'prometheus-postgres-exporter'
62
+ execute :sudo, 'systemctl', 'restart', 'prometheus-postgres-exporter'
49
63
  end
50
64
 
51
65
  puts " Metrics available at: http://#{host}:#{exporter_port}/metrics"
52
66
  end
67
+
68
+ def ensure_monitoring_user
69
+ monitoring_config = config.component_config(:monitoring)
70
+ monitoring_user = monitoring_config[:user] || 'postgres_exporter'
71
+ monitoring_password = normalize_monitoring_password(secrets.resolve('monitoring_password'))
72
+
73
+ sql = build_monitoring_user_sql(monitoring_user, monitoring_password)
74
+
75
+ ssh_executor.run_sql(config.primary_host, sql, postgres_user: 'postgres', port: 5432, tuples_only: false,
76
+ capture: false)
77
+ end
78
+
79
+ def build_monitoring_user_sql(user, password)
80
+ escaped_password = password.gsub("'", "''")
81
+
82
+ [
83
+ 'DO $$',
84
+ 'BEGIN',
85
+ " IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '#{user}') THEN",
86
+ " CREATE USER #{user} WITH LOGIN PASSWORD '#{escaped_password}';",
87
+ ' ELSE',
88
+ " ALTER USER #{user} WITH LOGIN PASSWORD '#{escaped_password}';",
89
+ ' END IF;',
90
+ 'END $$;',
91
+ '',
92
+ "GRANT pg_monitor TO #{user};",
93
+ ''
94
+ ].join("\n")
95
+ end
96
+
97
+ def configure_exporter_service(host, monitoring_config, exporter_port)
98
+ monitoring_user = monitoring_config[:user] || 'postgres_exporter'
99
+ monitoring_password = normalize_monitoring_password(secrets.resolve('monitoring_password'))
100
+
101
+ dsn = build_exporter_dsn(monitoring_config, monitoring_user, monitoring_password)
102
+ override = <<~CONF
103
+ [Service]
104
+ Environment="DATA_SOURCE_NAME=#{escape_systemd_env(dsn)}"
105
+ Environment="PG_EXPORTER_WEB_LISTEN_ADDRESS=:#{exporter_port}"
106
+ CONF
107
+
108
+ ssh_executor.execute_on_host(host) do
109
+ execute :sudo, 'mkdir', '-p', '/etc/systemd/system/prometheus-postgres-exporter.service.d'
110
+ upload! StringIO.new(override), '/tmp/prometheus-postgres-exporter.override'
111
+ execute :sudo, 'mv', '/tmp/prometheus-postgres-exporter.override',
112
+ '/etc/systemd/system/prometheus-postgres-exporter.service.d/override.conf'
113
+ execute :sudo, 'chown', 'root:root', '/etc/systemd/system/prometheus-postgres-exporter.service.d/override.conf'
114
+ execute :sudo, 'chmod', '600', '/etc/systemd/system/prometheus-postgres-exporter.service.d/override.conf'
115
+ execute :sudo, 'systemctl', 'daemon-reload'
116
+ end
117
+ end
118
+
119
+ def build_exporter_dsn(monitoring_config, monitoring_user, monitoring_password)
120
+ host = monitoring_config[:database_host] || 'localhost'
121
+ port = monitoring_config[:database_port] || 5432
122
+ database = monitoring_config[:database] || 'postgres'
123
+ sslmode = config.component_enabled?(:ssl) ? 'require' : 'prefer'
124
+
125
+ encoded_password = CGI.escape(monitoring_password.to_s)
126
+ "postgresql://#{monitoring_user}:#{encoded_password}@#{host}:#{port}/#{database}?sslmode=#{sslmode}"
127
+ end
128
+
129
+ def normalize_monitoring_password(raw_password)
130
+ password = raw_password.to_s.rstrip
131
+
132
+ raise Error, 'monitoring_password secret is missing (required for monitoring)' if password.empty?
133
+
134
+ password
135
+ end
136
+
137
+ def escape_systemd_env(value)
138
+ value.to_s.gsub('\\', '\\\\').gsub('"', '\\"')
139
+ end
53
140
  end
54
141
  end
55
142
  end
@@ -19,6 +19,7 @@ module ActivePostgres
19
19
  config.all_hosts.each do |host|
20
20
  ssh_executor.execute_on_host(host) do
21
21
  execute :sudo, 'apt-get', 'remove', '-y', 'pgbackrest'
22
+ execute :sudo, 'rm', '-f', '/etc/cron.d/pgbackrest-backup'
22
23
  end
23
24
  end
24
25
  end
@@ -27,6 +28,11 @@ module ActivePostgres
27
28
  puts "pgBackRest is a backup tool and doesn't run as a service."
28
29
  end
29
30
 
31
+ def install_on_standby(host)
32
+ puts " Installing pgBackRest on standby #{host}..."
33
+ install_on_host(host, create_stanza: false)
34
+ end
35
+
30
36
  def run_backup(type = 'full')
31
37
  puts "Running #{type} backup..."
32
38
  postgres_user = config.postgres_user
@@ -66,7 +72,7 @@ module ActivePostgres
66
72
  def install_on_host(host, create_stanza: true)
67
73
  puts " Installing pgBackRest on #{host}..."
68
74
 
69
- pgbackrest_config = config.component_config(:pgbackrest)
75
+ pgbackrest_config = secrets.resolve_value(config.component_config(:pgbackrest))
70
76
  postgres_user = config.postgres_user
71
77
  secrets_obj = secrets
72
78
  _ = pgbackrest_config # Used in ERB template
@@ -78,7 +84,8 @@ module ActivePostgres
78
84
  end
79
85
 
80
86
  # Upload configuration
81
- upload_template(host, 'pgbackrest.conf.erb', '/etc/pgbackrest.conf', binding, mode: '644')
87
+ upload_template(host, 'pgbackrest.conf.erb', '/etc/pgbackrest.conf', binding,
88
+ mode: '640', owner: "root:#{postgres_user}")
82
89
 
83
90
  ssh_executor.execute_on_host(host) do
84
91
  execute :sudo, 'rm', '-rf', '/var/lib/pgbackrest', '||', 'true'
@@ -99,6 +106,35 @@ module ActivePostgres
99
106
  execute :sudo, '-u', postgres_user, 'pgbackrest', '--stanza=main', 'stanza-create'
100
107
  end
101
108
  end
109
+
110
+ # Set up scheduled backups on primary only
111
+ if create_stanza && pgbackrest_config[:schedule]
112
+ setup_backup_schedule(host, pgbackrest_config[:schedule])
113
+ end
114
+ end
115
+
116
+ def setup_backup_schedule(host, schedule)
117
+ puts " Setting up backup schedule: #{schedule}"
118
+ postgres_user = config.postgres_user
119
+
120
+ # Create cron job for scheduled backups
121
+ # /etc/cron.d format requires username after time spec
122
+ cron_content = <<~CRON
123
+ # pgBackRest scheduled backups (managed by active_postgres)
124
+ SHELL=/bin/bash
125
+ PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
126
+ #{schedule} #{postgres_user} pgbackrest --stanza=main --type=full backup
127
+ CRON
128
+
129
+ # Install cron job in /etc/cron.d (system cron directory)
130
+ # Use a temp file + sudo mv to avoid shell redirection permission issues.
131
+ ssh_executor.upload_file(host, cron_content, '/etc/cron.d/pgbackrest-backup', mode: '644', owner: 'root:root')
132
+ end
133
+
134
+ def remove_backup_schedule(host)
135
+ ssh_executor.execute_on_host(host) do
136
+ execute :sudo, 'rm', '-f', '/etc/cron.d/pgbackrest-backup'
137
+ end
102
138
  end
103
139
  end
104
140
  end
@@ -14,6 +14,11 @@ module ActivePostgres
14
14
 
15
15
  config.all_hosts.each do |host|
16
16
  ssh_executor.execute_on_host(host) do
17
+ execute :sudo, 'systemctl', 'disable', '--now', 'pgbouncer-follow-primary.timer', '||', 'true'
18
+ execute :sudo, 'rm', '-f', '/usr/local/bin/pgbouncer-follow-primary',
19
+ '/etc/systemd/system/pgbouncer-follow-primary.service',
20
+ '/etc/systemd/system/pgbouncer-follow-primary.timer'
21
+ execute :sudo, 'systemctl', 'daemon-reload'
17
22
  execute :sudo, 'systemctl', 'stop', 'pgbouncer'
18
23
  execute :sudo, 'apt-get', 'remove', '-y', 'pgbouncer'
19
24
  end
@@ -53,12 +58,22 @@ module ActivePostgres
53
58
  puts " Installing PgBouncer on #{host}..."
54
59
 
55
60
  user_config = config.component_config(:pgbouncer)
61
+ follow_primary = user_config[:follow_primary] == true
62
+ if follow_primary && !config.component_enabled?(:repmgr)
63
+ raise Error, 'PgBouncer follow_primary requires repmgr to be enabled'
64
+ end
56
65
 
57
66
  max_connections = get_postgres_max_connections(host)
58
67
  optimal_pool = ConnectionPooler.calculate_optimal_pool_sizes(max_connections)
59
68
 
60
69
  pgbouncer_config = optimal_pool.merge(user_config)
70
+ pgbouncer_config[:database_host] = config.primary_replication_host if follow_primary
61
71
  ssl_enabled = config.component_enabled?(:ssl)
72
+ has_ca_cert = ssl_enabled && secrets.resolve('ssl_chain')
73
+ secrets_obj = secrets
74
+ _ = pgbouncer_config
75
+ _ = has_ca_cert
76
+ _ = secrets_obj
62
77
 
63
78
  puts " Calculated pool settings for max_connections=#{max_connections}"
64
79
 
@@ -76,6 +91,8 @@ module ActivePostgres
76
91
  execute :sudo, 'systemctl', 'enable', 'pgbouncer'
77
92
  execute :sudo, 'systemctl', 'restart', 'pgbouncer'
78
93
  end
94
+
95
+ install_follow_primary(host, pgbouncer_config) if follow_primary
79
96
  end
80
97
 
81
98
  def setup_ssl_certs(host)
@@ -130,10 +147,7 @@ module ActivePostgres
130
147
  def fetch_user_hash(backend, user, postgres_user)
131
148
  sql = build_user_hash_sql(user)
132
149
 
133
- backend.upload! StringIO.new(sql), '/tmp/get_user_hash.sql'
134
- backend.execute :chmod, '644', '/tmp/get_user_hash.sql'
135
- user_hash = backend.capture(:sudo, '-u', postgres_user, 'psql', '-t', '-f', '/tmp/get_user_hash.sql').strip
136
- backend.execute :rm, '-f', '/tmp/get_user_hash.sql'
150
+ user_hash = ssh_executor.run_sql_on_backend(backend, sql, postgres_user: postgres_user).to_s.strip
137
151
 
138
152
  if user_hash && !user_hash.empty?
139
153
  puts " ✓ Added #{user} to PgBouncer userlist"
@@ -167,6 +181,31 @@ module ActivePostgres
167
181
  warn ' Warning: No users added to userlist - connections may fail'
168
182
  end
169
183
  end
184
+
185
+ def install_follow_primary(host, pgbouncer_config)
186
+ interval = pgbouncer_config[:follow_primary_interval] || 5
187
+ interval = interval.to_i
188
+ interval = 5 if interval <= 0
189
+
190
+ repmgr_conf = '/etc/repmgr.conf'
191
+ postgres_user = config.postgres_user
192
+ _ = interval
193
+ _ = repmgr_conf
194
+ _ = postgres_user
195
+
196
+ upload_template(host, 'pgbouncer_follow_primary.sh.erb', '/usr/local/bin/pgbouncer-follow-primary', binding,
197
+ mode: '755', owner: 'root:root')
198
+ upload_template(host, 'pgbouncer-follow-primary.service.erb',
199
+ '/etc/systemd/system/pgbouncer-follow-primary.service', binding, mode: '644', owner: 'root:root')
200
+ upload_template(host, 'pgbouncer-follow-primary.timer.erb',
201
+ '/etc/systemd/system/pgbouncer-follow-primary.timer', binding, mode: '644', owner: 'root:root')
202
+
203
+ ssh_executor.execute_on_host(host) do
204
+ execute :sudo, 'systemctl', 'daemon-reload'
205
+ execute :sudo, 'systemctl', 'enable', '--now', 'pgbouncer-follow-primary.timer'
206
+ execute :sudo, 'systemctl', 'start', 'pgbouncer-follow-primary.service'
207
+ end
208
+ end
170
209
  end
171
210
  end
172
211
  end