active_postgres 0.9.4 → 0.9.5

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: b0966aed96fa6f906e7b048ef7804a0b9994c54f841f8dc9f00425f57f565a71
4
- data.tar.gz: 86582950fc4afa733cba7052682dbaf0f3e76275c9397559f2d3398c8f92da86
3
+ metadata.gz: 8f31b928d75edd1c1c63a6534023a3c7255f4bce000487da5fb39ddd0bd357fb
4
+ data.tar.gz: ee835da34ead4b417ac2c5087e9fae0db12641c7dddb44e91fe795d083f4e61a
5
5
  SHA512:
6
- metadata.gz: 18644b19453727a1b14918f9bc7d055fdf2c8cbaab962d94acdb058be4fa6b969e72e569ea5e1a2f0848cc45433e35c47dfa69af699370cb267b64c0a61114d5
7
- data.tar.gz: '08b112f2121747a9b970809bf0c662dfbed99166edf6e3b18e88daab4fff0a65fcae13821c37fa69af218497b9ac05978d2bf6cd157c197d0f719f968088f407'
6
+ metadata.gz: 81cc77508c361ab128d840dbaac52ae9f3fc50b431fa971aaede91fc573ed9186521a53f8098e038515b6c96c82141100fd7ce797b0eae5c4b2840bd01330212
7
+ data.tar.gz: bcdeb64f655cbc0e702f5b70fc3c749daa103ac74945a9432627aeff6926de9b1974fa52547b32e1d99acec687d30b252473a15a75ffadd299e6bc951e9e2bf2
@@ -212,7 +212,7 @@ module ActivePostgres
212
212
  env_content = log_archive_env(pgbackrest_config, log_archive_config, host)
213
213
  cron_content = log_archive_cron(log_archive_config)
214
214
 
215
- install_apt_packages(host, 'awscli')
215
+ install_apt_packages(host, 's3cmd')
216
216
 
217
217
  ssh_executor.upload_file(host, env_content, LOG_ARCHIVE_ENV_FILE, mode: '640', owner: "root:#{postgres_user}")
218
218
 
@@ -8,12 +8,16 @@ module ActivePostgres
8
8
  is_standby = config.standby_hosts.include?(host)
9
9
  install_on_host(host, is_standby: is_standby)
10
10
  end
11
+
12
+ pgbouncer_app_hosts.each do |app_host|
13
+ install_on_app_host(app_host)
14
+ end
11
15
  end
12
16
 
13
17
  def uninstall
14
18
  puts 'Uninstalling PgBouncer...'
15
19
 
16
- config.all_hosts.each do |host|
20
+ (config.all_hosts + pgbouncer_app_hosts.map { |app_host| app_host.fetch('host') }).each do |host|
17
21
  ssh_executor.execute_on_host(host) do
18
22
  execute :sudo, 'systemctl', 'disable', '--now', 'pgbouncer-follow-primary.timer', '||', 'true'
19
23
  execute :sudo, 'rm', '-f', '/usr/local/bin/pgbouncer-follow-primary',
@@ -29,7 +33,7 @@ module ActivePostgres
29
33
  def restart
30
34
  puts 'Restarting PgBouncer...'
31
35
 
32
- config.all_hosts.each do |host|
36
+ (config.all_hosts + pgbouncer_app_hosts.map { |app_host| app_host.fetch('host') }).each do |host|
33
37
  ssh_executor.execute_on_host(host) do
34
38
  execute :sudo, 'systemctl', 'restart', 'pgbouncer'
35
39
  end
@@ -46,6 +50,15 @@ module ActivePostgres
46
50
  execute :sudo, 'systemctl', 'reload', 'pgbouncer'
47
51
  end
48
52
  end
53
+
54
+ pgbouncer_app_hosts.each do |app_host|
55
+ host = app_host.fetch('host')
56
+ create_userlist_from_primary(host)
57
+
58
+ ssh_executor.execute_on_host(host) do
59
+ execute :sudo, 'systemctl', 'reload', 'pgbouncer'
60
+ end
61
+ end
49
62
  end
50
63
 
51
64
  def install_on_standby(standby_host)
@@ -55,6 +68,12 @@ module ActivePostgres
55
68
 
56
69
  private
57
70
 
71
+ def pgbouncer_app_hosts
72
+ return config.pgbouncer_app_hosts if config.respond_to?(:pgbouncer_app_hosts)
73
+
74
+ []
75
+ end
76
+
58
77
  def install_on_host(host, is_standby: false)
59
78
  puts " Installing PgBouncer on #{host}..."
60
79
 
@@ -106,6 +125,45 @@ module ActivePostgres
106
125
  install_follow_primary(host, pgbouncer_config) if follow_primary
107
126
  end
108
127
 
128
+ def install_on_app_host(app_host)
129
+ host = app_host.fetch('host')
130
+ puts " Installing PgBouncer on app host #{host}..."
131
+
132
+ user_config = config.component_config(:pgbouncer)
133
+ max_connections = get_postgres_max_connections(config.primary_host)
134
+ optimal_pool = ConnectionPooler.calculate_optimal_pool_sizes(max_connections)
135
+
136
+ pgbouncer_config = optimal_pool.merge(user_config)
137
+ pgbouncer_config[:database_host] = app_host_value(app_host, :database_host) || pgbouncer_primary_record
138
+ pgbouncer_config[:database_port] = app_host_value(app_host, :database_port) || pgbouncer_config[:database_port] || 5432
139
+ pgbouncer_config[:listen_addr] = app_host_value(app_host, :listen_addr) || pgbouncer_config[:app_listen_addr] || '127.0.0.1'
140
+ pgbouncer_config[:listen_port] = app_host_value(app_host, :listen_port) || pgbouncer_config[:app_listen_port] ||
141
+ pgbouncer_config[:listen_port] || 6432
142
+ ssl_enabled = config.component_enabled?(:ssl)
143
+ has_ca_cert = ssl_enabled && secrets.resolve('ssl_chain')
144
+ secrets_obj = secrets
145
+ _ = pgbouncer_config
146
+ _ = has_ca_cert
147
+ _ = secrets_obj
148
+
149
+ puts " Calculated app pool settings for max_connections=#{max_connections}"
150
+
151
+ install_apt_packages(host, 'pgbouncer', 'postgresql-client')
152
+
153
+ upload_template(host, 'pgbouncer.ini.erb', '/etc/pgbouncer/pgbouncer.ini', binding, mode: '644')
154
+
155
+ setup_app_ssl_certs(host) if ssl_enabled
156
+
157
+ create_userlist_from_primary(host)
158
+
159
+ ssh_executor.execute_on_host(host) do
160
+ execute :sudo, 'systemctl', 'enable', 'pgbouncer'
161
+ execute :sudo, 'systemctl', 'restart', 'pgbouncer'
162
+ end
163
+
164
+ install_follow_dns_primary(host, pgbouncer_config)
165
+ end
166
+
109
167
  def follow_primary_for?(host, is_standby:, user_config:)
110
168
  if is_standby
111
169
  standby_config = config.standby_config_for(host) || {}
@@ -135,6 +193,32 @@ module ActivePostgres
135
193
  end
136
194
  end
137
195
 
196
+ def setup_app_ssl_certs(host)
197
+ puts ' Setting up SSL certificates for app PgBouncer...'
198
+
199
+ ssl_cert = secrets.resolve('ssl_cert')
200
+ ssl_key = secrets.resolve('ssl_key')
201
+ ssl_chain = secrets.resolve('ssl_chain')
202
+
203
+ unless ssl_cert && ssl_key
204
+ raise Error, 'PgBouncer app_hosts require ssl_cert and ssl_key secrets when ssl is enabled'
205
+ end
206
+
207
+ full_cert = if ssl_chain
208
+ "#{ssl_cert.strip}\n#{ssl_chain.strip}\n"
209
+ else
210
+ ssl_cert
211
+ end
212
+
213
+ ssh_executor.execute_on_host(host) do
214
+ execute :sudo, 'mkdir', '-p', '/etc/pgbouncer'
215
+ end
216
+
217
+ ssh_executor.upload_file(host, full_cert, '/etc/pgbouncer/server.crt', mode: '644', owner: 'postgres:postgres')
218
+ ssh_executor.upload_file(host, ssl_key, '/etc/pgbouncer/server.key', mode: '600', owner: 'postgres:postgres')
219
+ ssh_executor.upload_file(host, ssl_chain, '/etc/pgbouncer/ca.crt', mode: '644', owner: 'postgres:postgres') if ssl_chain
220
+ end
221
+
138
222
  def get_postgres_max_connections(host)
139
223
  # Try to get max_connections from running PostgreSQL
140
224
  postgres_user = config.postgres_user
@@ -154,18 +238,43 @@ module ActivePostgres
154
238
  def create_userlist(host)
155
239
  puts ' Creating PgBouncer userlist with database users...'
156
240
 
157
- postgres_user = config.postgres_user
158
- app_user = config.app_user
159
- users_to_add = [postgres_user, (app_user if app_user != postgres_user)].compact
160
- pgbouncer = self
241
+ component = self
161
242
 
162
243
  ssh_executor.execute_on_host(host) do
163
- backend = self
164
- userlist_entries = users_to_add.filter_map { |user| pgbouncer.send(:fetch_user_hash, backend, user, postgres_user) }
165
- pgbouncer.send(:write_userlist_file, backend, userlist_entries)
244
+ userlist_entries = component.send(:userlist_entries_from_backend, self)
245
+ component.send(:write_userlist_file, self, userlist_entries)
166
246
  end
167
247
  end
168
248
 
249
+ def create_userlist_from_primary(host)
250
+ puts ' Creating app PgBouncer userlist from primary database users...'
251
+
252
+ userlist_entries = nil
253
+ component = self
254
+
255
+ ssh_executor.execute_on_host(config.primary_host) do
256
+ userlist_entries = component.send(:userlist_entries_from_backend, self)
257
+ end
258
+
259
+ ssh_executor.execute_on_host(host) do
260
+ component.send(:write_userlist_file, self, userlist_entries || [])
261
+ end
262
+ end
263
+
264
+ def userlist_entries_from_backend(backend)
265
+ postgres_user = config.postgres_user
266
+ users_to_add = pgbouncer_database_users
267
+
268
+ users_to_add.filter_map { |user| fetch_user_hash(backend, user, postgres_user) }
269
+ end
270
+
271
+ def pgbouncer_database_users
272
+ postgres_user = config.postgres_user
273
+ app_user = config.app_user
274
+
275
+ [postgres_user, (app_user if app_user != postgres_user)].compact
276
+ end
277
+
169
278
  def fetch_user_hash(backend, user, postgres_user)
170
279
  sql = build_user_hash_sql(user)
171
280
 
@@ -241,6 +350,40 @@ module ActivePostgres
241
350
  execute :sudo, 'systemctl', 'start', 'pgbouncer-follow-primary.service'
242
351
  end
243
352
  end
353
+
354
+ def install_follow_dns_primary(host, pgbouncer_config)
355
+ interval = pgbouncer_config[:follow_primary_interval] || 5
356
+ interval = interval.to_i
357
+ interval = 5 if interval <= 0
358
+ primary_record = pgbouncer_config[:database_host]
359
+ _ = interval
360
+ _ = primary_record
361
+ _ = pgbouncer_config
362
+
363
+ upload_template(host, 'pgbouncer_follow_dns_primary.sh.erb', '/usr/local/bin/pgbouncer-follow-primary',
364
+ binding, mode: '755', owner: 'root:root')
365
+ upload_template(host, 'pgbouncer-follow-primary.service.erb',
366
+ '/etc/systemd/system/pgbouncer-follow-primary.service', binding, mode: '644', owner: 'root:root')
367
+ upload_template(host, 'pgbouncer-follow-primary.timer.erb',
368
+ '/etc/systemd/system/pgbouncer-follow-primary.timer', binding, mode: '644', owner: 'root:root')
369
+
370
+ ssh_executor.execute_on_host(host) do
371
+ execute :sudo, 'systemctl', 'daemon-reload'
372
+ execute :sudo, 'systemctl', 'enable', '--now', 'pgbouncer-follow-primary.timer'
373
+ execute :sudo, 'systemctl', 'start', 'pgbouncer-follow-primary.service'
374
+ end
375
+ end
376
+
377
+ def pgbouncer_primary_record
378
+ return config.pgbouncer_primary_record if config.respond_to?(:pgbouncer_primary_record)
379
+
380
+ dns_failover = config.component_config(:repmgr)[:dns_failover] || {}
381
+ dns_failover[:primary_record] || config.primary_replication_host
382
+ end
383
+
384
+ def app_host_value(app_host, key)
385
+ app_host[key.to_s] || app_host[key]
386
+ end
244
387
  end
245
388
  end
246
389
  end
@@ -39,6 +39,35 @@ module ActivePostgres
39
39
  [primary_host] + standby_hosts
40
40
  end
41
41
 
42
+ def pgbouncer_app_hosts
43
+ hosts = component_config(:pgbouncer)[:app_hosts] || component_config(:pgbouncer)['app_hosts'] || []
44
+ hosts = [hosts] unless hosts.is_a?(Array)
45
+
46
+ hosts.filter_map do |entry|
47
+ case entry
48
+ when String
49
+ entry.strip.empty? ? nil : { 'host' => entry.strip }
50
+ when Hash
51
+ host = entry['host'] || entry[:host] || entry['ip'] || entry[:ip]
52
+ next if host.to_s.strip.empty?
53
+
54
+ entry.merge('host' => host.to_s.strip)
55
+ end
56
+ end
57
+ end
58
+
59
+ def pgbouncer_primary_record
60
+ pgbouncer = component_config(:pgbouncer)
61
+ explicit = pgbouncer[:primary_record] || pgbouncer['primary_record']
62
+ return explicit.to_s.strip unless explicit.to_s.strip.empty?
63
+
64
+ dns_failover = component_config(:repmgr)[:dns_failover] || component_config(:repmgr)['dns_failover'] || {}
65
+ record = dns_failover[:primary_record] || dns_failover['primary_record']
66
+ return record.to_s.strip unless record.to_s.strip.empty?
67
+
68
+ primary_replication_host
69
+ end
70
+
42
71
  def primary_host
43
72
  @primary['host']
44
73
  end
@@ -114,6 +143,13 @@ module ActivePostgres
114
143
  end
115
144
  end
116
145
 
146
+ if component_enabled?(:pgbouncer)
147
+ pgbouncer_app_hosts.each do |app_host|
148
+ host = app_host['host'] || app_host[:host]
149
+ raise Error, 'pgbouncer.app_hosts entries must include host' if host.to_s.strip.empty?
150
+ end
151
+ end
152
+
117
153
  if component_enabled?(:repmgr)
118
154
  dns_failover = component_config(:repmgr)[:dns_failover]
119
155
  if dns_failover && dns_failover[:enabled]
@@ -96,6 +96,8 @@ module ActivePostgres
96
96
  return if defined?(::Rails) && ::Rails.respond_to?(:application) && ::Rails.application
97
97
 
98
98
  @rails_boot_attempted = true
99
+ ENV['RAILS_ENV'] ||= config.environment
100
+ ENV['BORING_ENVIRONMENT'] ||= config.environment
99
101
 
100
102
  # Try loading just the Rails application (without full initialization)
101
103
  # This makes Rails.application available for credential access
@@ -1,3 +1,3 @@
1
1
  module ActivePostgres
2
- VERSION = '0.9.4'.freeze
2
+ VERSION = '0.9.5'.freeze
3
3
  end
@@ -57,6 +57,8 @@ log-path=/var/log/pgbackrest
57
57
 
58
58
  # Process settings
59
59
  process-max=<%= pgbackrest_config[:process_max] || 2 %>
60
+ archive-async=y
61
+ spool-path=/var/spool/pgbackrest
60
62
 
61
63
  # Start/Stop (for consistent backups)
62
64
  start-fast=y
@@ -68,4 +70,3 @@ pg1-port=5432
68
70
  pg1-socket-path=/var/run/postgresql
69
71
  pg1-user=<%= config.postgres_user %>
70
72
 
71
-
@@ -36,6 +36,8 @@ client_idle_timeout = <%= pgbouncer_config[:client_idle_timeout] || 0 %>
36
36
  client_login_timeout = <%= pgbouncer_config[:client_login_timeout] || 60 %>
37
37
  query_timeout = <%= pgbouncer_config[:query_timeout] || 0 %>
38
38
  query_wait_timeout = <%= pgbouncer_config[:query_wait_timeout] || 120 %>
39
+ dns_max_ttl = <%= pgbouncer_config[:dns_max_ttl] || 5 %>
40
+ dns_nxdomain_ttl = <%= pgbouncer_config[:dns_nxdomain_ttl] || 15 %>
39
41
 
40
42
  # Logging and administration
41
43
  admin_users = <%= pgbouncer_config[:admin_users] || config.postgres_user %>
@@ -62,4 +64,3 @@ pidfile = <%= pgbouncer_config[:pidfile] %>
62
64
  logfile = <%= pgbouncer_config[:logfile] %>
63
65
  <% end %>
64
66
 
65
-
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ PRIMARY_RECORD="<%= primary_record %>"
5
+ PGBOUNCER_INI="/etc/pgbouncer/pgbouncer.ini"
6
+ PGBOUNCER_SERVICE="pgbouncer"
7
+ STATE_DIR="/var/lib/active-postgres"
8
+ STATE_FILE="${STATE_DIR}/pgbouncer-primary-ip"
9
+ LOG_TAG="pgbouncer-follow-primary"
10
+
11
+ resolved_ip=$(getent ahostsv4 "$PRIMARY_RECORD" | awk '{print $1; exit}' || true)
12
+ if [[ -z "$resolved_ip" ]]; then
13
+ /usr/bin/logger -t "$LOG_TAG" "Could not resolve ${PRIMARY_RECORD}"
14
+ exit 1
15
+ fi
16
+
17
+ current_host=$(awk -F'host=' '/^\\* = host=/{print $2}' "$PGBOUNCER_INI" | awk '{print $1}')
18
+ previous_ip=""
19
+ if [[ -f "$STATE_FILE" ]]; then
20
+ previous_ip=$(cat "$STATE_FILE")
21
+ fi
22
+
23
+ changed=0
24
+ if [[ "$current_host" != "$PRIMARY_RECORD" ]]; then
25
+ /usr/bin/sed -i -E "s/^(\\* = host=)[^ ]+/\\1${PRIMARY_RECORD}/" "$PGBOUNCER_INI"
26
+ changed=1
27
+ fi
28
+
29
+ if [[ "$previous_ip" != "$resolved_ip" ]]; then
30
+ changed=1
31
+ fi
32
+
33
+ /usr/bin/install -d -m 0755 "$STATE_DIR"
34
+ printf "%s\n" "$resolved_ip" > "$STATE_FILE"
35
+
36
+ if [[ "$changed" == "1" ]]; then
37
+ /usr/bin/systemctl restart "$PGBOUNCER_SERVICE"
38
+ /usr/bin/logger -t "$LOG_TAG" "Refreshed PgBouncer primary target ${PRIMARY_RECORD} (${resolved_ip})"
39
+ fi
@@ -46,12 +46,43 @@ if [[ ${#tar_paths[@]} -eq 0 ]]; then
46
46
  exit 0
47
47
  fi
48
48
 
49
- tar -czf "$archive" -C / "${tar_paths[@]}"
49
+ set +e
50
+ tar --warning=no-file-changed --ignore-failed-read -czf "$archive" -C / "${tar_paths[@]}"
51
+ tar_status=$?
52
+ set -e
50
53
 
51
- destination="s3://${AWS_BUCKET}/${PREFIX%/}/${NODE}/${DATE_PATH}/$(basename "$archive")"
52
- aws_args=(s3 cp "$archive" "$destination" --only-show-errors)
53
- if [[ -n "${AWS_ENDPOINT_URL:-}" ]]; then
54
- aws_args+=(--endpoint-url "$AWS_ENDPOINT_URL")
54
+ if [[ $tar_status -gt 1 ]]; then
55
+ exit "$tar_status"
55
56
  fi
56
57
 
57
- aws "${aws_args[@]}"
58
+ if [[ ! -s "$archive" ]]; then
59
+ exit 0
60
+ fi
61
+
62
+ destination="s3://${AWS_BUCKET}/${PREFIX%/}/${NODE}/${DATE_PATH}/$(basename "$archive")"
63
+ if command -v aws >/dev/null 2>&1; then
64
+ aws_args=(s3 cp "$archive" "$destination" --only-show-errors)
65
+ if [[ -n "${AWS_ENDPOINT_URL:-}" ]]; then
66
+ aws_args+=(--endpoint-url "$AWS_ENDPOINT_URL")
67
+ fi
68
+
69
+ aws "${aws_args[@]}"
70
+ elif command -v s3cmd >/dev/null 2>&1; then
71
+ s3cmd_args=(
72
+ --access_key="$AWS_ACCESS_KEY_ID"
73
+ --secret_key="$AWS_SECRET_ACCESS_KEY"
74
+ --region="${AWS_DEFAULT_REGION:-us-east-1}"
75
+ --no-progress
76
+ )
77
+
78
+ if [[ -n "${AWS_ENDPOINT_URL:-}" ]]; then
79
+ endpoint="${AWS_ENDPOINT_URL#https://}"
80
+ endpoint="${endpoint#http://}"
81
+ s3cmd_args+=(--host="$endpoint" --host-bucket="${endpoint}/%(bucket)")
82
+ fi
83
+
84
+ s3cmd "${s3cmd_args[@]}" put "$archive" "$destination"
85
+ else
86
+ echo "missing aws or s3cmd command for PostgreSQL log archive" >&2
87
+ exit 1
88
+ fi
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_postgres
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.4
4
+ version: 0.9.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - BoringCache
@@ -199,6 +199,7 @@ files:
199
199
  - templates/pgbouncer-follow-primary.service.erb
200
200
  - templates/pgbouncer-follow-primary.timer.erb
201
201
  - templates/pgbouncer.ini.erb
202
+ - templates/pgbouncer_follow_dns_primary.sh.erb
202
203
  - templates/pgbouncer_follow_primary.sh.erb
203
204
  - templates/postgres_log_archive.sh.erb
204
205
  - templates/postgresql.conf.erb