active_postgres 0.9.5 → 0.9.6

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: 8f31b928d75edd1c1c63a6534023a3c7255f4bce000487da5fb39ddd0bd357fb
4
- data.tar.gz: ee835da34ead4b417ac2c5087e9fae0db12641c7dddb44e91fe795d083f4e61a
3
+ metadata.gz: 5d06925c9a64ca3f5b96402257fda9ad13f32bbd0e3ac5377032d737c71fb2ea
4
+ data.tar.gz: 19cde3ee7e86d325378661aa72350d51d1bfebf33a2e6f03a1c90d34c987a997
5
5
  SHA512:
6
- metadata.gz: 81cc77508c361ab128d840dbaac52ae9f3fc50b431fa971aaede91fc573ed9186521a53f8098e038515b6c96c82141100fd7ce797b0eae5c4b2840bd01330212
7
- data.tar.gz: bcdeb64f655cbc0e702f5b70fc3c749daa103ac74945a9432627aeff6926de9b1974fa52547b32e1d99acec687d30b252473a15a75ffadd299e6bc951e9e2bf2
6
+ metadata.gz: 2dbcc33b6ad2ffadeeea4145bc5bc36910203002af908030893298df2005089e87fa8e5d465a425ffae11c60ee01b66f301e1ae594e3bb662d747c0c342b20cb
7
+ data.tar.gz: 2772e4a192f87c60d54ccaaeb5ea2952f7a98ce34f3daec5839e77758179f75f3f9ac09a76ab74bc929311b8b417e074e5ae156f286bbba55423c3a923b47d77
data/CHANGELOG.md ADDED
@@ -0,0 +1,23 @@
1
+ # Changelog
2
+
3
+ All notable changes to Active Postgres will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project follows [Semantic Versioning](https://semver.org/).
7
+
8
+ ## Unreleased
9
+
10
+ ## 0.9.6 - 2026-08-20
11
+
12
+ ### Changed
13
+
14
+ - Require Ruby 4.0 or newer and Rails 8.1 or newer for Rails integration.
15
+ - Align runtime and development dependencies with the maintained toolchain.
16
+ - Add blocking test, lint, dependency-audit, and package-build CI checks.
17
+ - Add automated Bundler and GitHub Actions dependency updates.
18
+ - Document network, credential, failover, and recovery responsibilities.
19
+
20
+ ## 0.9.5
21
+
22
+ - Current published release. See the repository history for changes included in
23
+ earlier releases.
data/README.md CHANGED
@@ -1,6 +1,10 @@
1
1
  # active_postgres
2
2
 
3
- Production-grade PostgreSQL HA for Rails.
3
+ PostgreSQL high-availability automation for Ruby and Rails applications.
4
+
5
+ Active Postgres configures hosts you control over SSH. It is an operations tool,
6
+ not a managed database service: you remain responsible for network isolation,
7
+ capacity planning, monitoring, backups, recovery drills, and failover decisions.
4
8
 
5
9
  ## Features
6
10
 
@@ -272,11 +276,23 @@ User.all # → Replica
272
276
 
273
277
  ## Requirements
274
278
 
275
- - Ruby 3.0+
276
- - PostgreSQL 12+
279
+ - Ruby 4.0+
280
+ - A PostgreSQL version supported by the target Linux distribution
277
281
  - Ubuntu 20.04+ / Debian 11+ with systemd
278
282
  - SSH key-based authentication (hosts must be in `~/.ssh/known_hosts`)
279
- - Rails 6.0+ (optional)
283
+ - Rails 8.1+ (optional)
284
+
285
+ ## Operational safety
286
+
287
+ - Keep PostgreSQL, PgBouncer, exporters, and backup endpoints behind a private
288
+ network or narrowly scoped firewall rules.
289
+ - Test promotion, application reconnection, backup restore, and point-in-time
290
+ recovery in a disposable environment before using them in production.
291
+ - Review generated configuration and package changes before each rollout;
292
+ automatic failover cannot determine whether an application is safe to write
293
+ after a network partition.
294
+ - Store database, replication, monitoring, and backup credentials in a secret
295
+ manager. Do not commit resolved values or generated configuration.
280
296
 
281
297
  ### SSH host key verification
282
298
 
data/SECURITY.md ADDED
@@ -0,0 +1,21 @@
1
+ # Security Policy
2
+
3
+ ## Reporting a Vulnerability
4
+
5
+ Please do not open a public issue for a potential vulnerability. Email
6
+ [security@boringcache.com](mailto:security@boringcache.com) or use
7
+ [GitHub private vulnerability reporting](https://github.com/boringcache/active_postgres/security/advisories/new).
8
+
9
+ Include the affected version, expected impact, and enough reproduction detail
10
+ for us to investigate. Do not include database credentials, private keys,
11
+ backups, customer data, or production configuration.
12
+
13
+ ## Supported Versions
14
+
15
+ Only the latest published release is supported. Security fixes are normally
16
+ shipped in a new release rather than backported.
17
+
18
+ ## Scope
19
+
20
+ Reports about PostgreSQL authentication, replication, backup/restore, SSH host
21
+ verification, secret handling, generated SQL, or release integrity are welcome.
@@ -1,3 +1,5 @@
1
+ require 'erb'
2
+
1
3
  module ActivePostgres
2
4
  module Components
3
5
  class Base
@@ -136,7 +136,7 @@ module ActivePostgres
136
136
  end
137
137
 
138
138
  def ensure_standby_compatible(pg_config)
139
- primary_settings = get_primary_replication_settings
139
+ primary_settings = primary_replication_settings
140
140
  return pg_config if primary_settings.empty?
141
141
 
142
142
  adjustments = []
@@ -154,17 +154,24 @@ module ActivePostgres
154
154
  end
155
155
 
156
156
  if adjustments.any?
157
- puts " ⚠️ Adjusting standby settings to match primary minimum:"
157
+ puts ' ⚠️ Adjusting standby settings to match primary minimum:'
158
158
  adjustments.each { |adj| puts " #{adj}" }
159
159
  end
160
160
 
161
161
  pg_config
162
162
  end
163
163
 
164
- def get_primary_replication_settings
164
+ def primary_replication_settings
165
165
  @primary_replication_settings ||= begin
166
166
  settings = {}
167
- sql = "SELECT name || '=' || setting FROM pg_settings WHERE name IN ('max_connections', 'max_worker_processes', 'max_wal_senders', 'max_prepared_transactions', 'max_locks_per_transaction')"
167
+ setting_names = %w[
168
+ max_connections
169
+ max_worker_processes
170
+ max_wal_senders
171
+ max_prepared_transactions
172
+ max_locks_per_transaction
173
+ ]
174
+ sql = "SELECT name || '=' || setting FROM pg_settings WHERE name IN (#{setting_names.map { |name| "'#{name}'" }.join(', ')})"
168
175
  result = ssh_executor.run_sql(config.primary_host, sql)
169
176
  result.strip.split("\n").each do |line|
170
177
  name, val = line.split('=')
@@ -1,4 +1,4 @@
1
- require 'cgi'
1
+ require 'cgi/escape'
2
2
 
3
3
  module ActivePostgres
4
4
  module Components
@@ -83,7 +83,7 @@ module ActivePostgres
83
83
  sql = build_monitoring_user_sql(monitoring_user, monitoring_password)
84
84
 
85
85
  ssh_executor.run_sql(config.primary_host, sql, postgres_user: 'postgres', port: 5432, tuples_only: false,
86
- capture: false)
86
+ capture: false)
87
87
  end
88
88
 
89
89
  def build_monitoring_user_sql(user, password)
@@ -3,9 +3,9 @@ require 'shellwords'
3
3
  module ActivePostgres
4
4
  module Components
5
5
  class PgBackRest < Base
6
- LOG_ARCHIVE_CRON_FILE = '/etc/cron.d/postgres-log-archive'
7
- LOG_ARCHIVE_ENV_FILE = '/etc/active-postgres-log-archive.env'
8
- LOG_ARCHIVE_SCRIPT = '/usr/local/bin/active-postgres-archive-logs'
6
+ LOG_ARCHIVE_CRON_FILE = '/etc/cron.d/postgres-log-archive'.freeze
7
+ LOG_ARCHIVE_ENV_FILE = '/etc/active-postgres-log-archive.env'.freeze
8
+ LOG_ARCHIVE_SCRIPT = '/usr/local/bin/active-postgres-archive-logs'.freeze
9
9
 
10
10
  def install
11
11
  puts 'Installing pgBackRest for backups...'
@@ -135,9 +135,7 @@ module ActivePostgres
135
135
  execute :sudo, 'chown', "#{postgres_user}:#{postgres_user}", '/var/spool/pgbackrest'
136
136
 
137
137
  # Only create stanza on primary - standbys share the same backup repo
138
- if create_stanza
139
- execute :sudo, '-u', postgres_user, 'pgbackrest', '--stanza=main', 'stanza-create'
140
- end
138
+ execute :sudo, '-u', postgres_user, 'pgbackrest', '--stanza=main', 'stanza-create' if create_stanza
141
139
  end
142
140
 
143
141
  if log_archive_enabled?(pgbackrest_config)
@@ -167,12 +165,8 @@ module ActivePostgres
167
165
  schedule_incremental = pgbackrest_config[:schedule_incremental]
168
166
 
169
167
  schedules = []
170
- if schedule_full
171
- schedules << { type: 'full', schedule: schedule_full, file: '/etc/cron.d/pgbackrest-backup' }
172
- end
173
- if schedule_incremental
174
- schedules << { type: 'incremental', schedule: schedule_incremental, file: '/etc/cron.d/pgbackrest-backup-incremental' }
175
- end
168
+ schedules << { type: 'full', schedule: schedule_full, file: '/etc/cron.d/pgbackrest-backup' } if schedule_full
169
+ schedules << { type: 'incr', schedule: schedule_incremental, file: '/etc/cron.d/pgbackrest-backup-incremental' } if schedule_incremental
176
170
 
177
171
  schedules
178
172
  end
@@ -204,9 +198,7 @@ module ActivePostgres
204
198
 
205
199
  def setup_log_archive(host, pgbackrest_config)
206
200
  log_archive_config = pgbackrest_config[:log_archive] || {}
207
- unless pgbackrest_config[:repo_type] == 's3'
208
- raise Error, 'pgbackrest.log_archive requires pgbackrest.repo_type: s3'
209
- end
201
+ raise Error, 'pgbackrest.log_archive requires pgbackrest.repo_type: s3' unless pgbackrest_config[:repo_type] == 's3'
210
202
 
211
203
  postgres_user = config.postgres_user
212
204
  env_content = log_archive_env(pgbackrest_config, log_archive_config, host)
@@ -260,13 +252,13 @@ module ActivePostgres
260
252
  'POSTGRES_LOG_ARCHIVE_NODE' => config.node_label_for(host) || host
261
253
  }.compact
262
254
 
263
- env.map { |key, value| "#{key}=#{Shellwords.escape(value.to_s)}" }.join("\n") + "\n"
255
+ "#{env.map { |key, value| "#{key}=#{Shellwords.escape(value.to_s)}" }.join("\n")}\n"
264
256
  end
265
257
 
266
258
  def s3_endpoint_url(pgbackrest_config)
267
259
  endpoint = pgbackrest_config[:s3_endpoint]
268
260
  return nil if endpoint.to_s.empty?
269
- return endpoint if endpoint.match?(/\Ahttps?:\/\//)
261
+ return endpoint if endpoint.match?(%r{\Ahttps?://})
270
262
 
271
263
  "https://#{endpoint}"
272
264
  end
@@ -81,9 +81,7 @@ module ActivePostgres
81
81
 
82
82
  follow_primary = follow_primary_for?(host, is_standby: is_standby, user_config: user_config)
83
83
 
84
- if follow_primary && !config.component_enabled?(:repmgr)
85
- raise Error, 'PgBouncer follow_primary requires repmgr to be enabled'
86
- end
84
+ raise Error, 'PgBouncer follow_primary requires repmgr to be enabled' if follow_primary && !config.component_enabled?(:repmgr)
87
85
 
88
86
  max_connections = get_postgres_max_connections(host)
89
87
  optimal_pool = ConnectionPooler.calculate_optimal_pool_sizes(max_connections)
@@ -188,9 +186,9 @@ module ActivePostgres
188
186
  end
189
187
 
190
188
  ssl_chain = secrets.resolve('ssl_chain')
191
- if ssl_chain
192
- ssh_executor.upload_file(host, ssl_chain, '/etc/pgbouncer/ca.crt', mode: '644', owner: 'postgres:postgres')
193
- end
189
+ return unless ssl_chain
190
+
191
+ ssh_executor.upload_file(host, ssl_chain, '/etc/pgbouncer/ca.crt', mode: '644', owner: 'postgres:postgres')
194
192
  end
195
193
 
196
194
  def setup_app_ssl_certs(host)
@@ -200,9 +198,7 @@ module ActivePostgres
200
198
  ssl_key = secrets.resolve('ssl_key')
201
199
  ssl_chain = secrets.resolve('ssl_chain')
202
200
 
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
201
+ raise Error, 'PgBouncer app_hosts require ssl_cert and ssl_key secrets when ssl is enabled' unless ssl_cert && ssl_key
206
202
 
207
203
  full_cert = if ssl_chain
208
204
  "#{ssl_cert.strip}\n#{ssl_chain.strip}\n"
@@ -114,6 +114,7 @@ module ActivePostgres
114
114
  if replication_user == repmgr_user && replication_password != repmgr_password
115
115
  raise Error, 'replication_user matches repmgr user but passwords differ. Use a distinct replication_user or the same password.'
116
116
  end
117
+
117
118
  effective_replication_password = replication_user == repmgr_user ? repmgr_password : replication_password
118
119
 
119
120
  # Variables used in ERB templates via binding
@@ -174,13 +175,13 @@ module ActivePostgres
174
175
  info 'Creating repmgr database and user...'
175
176
  repmgr_sql = repmgr_component.send(:build_repmgr_setup_sql, repmgr_user, repmgr_db, repmgr_password)
176
177
  executor.run_sql_on_backend(self, repmgr_sql, postgres_user: 'postgres', port: 5432, tuples_only: false,
177
- capture: false)
178
+ capture: false)
178
179
 
179
180
  if replication_user != repmgr_user
180
181
  info 'Ensuring replication user exists...'
181
182
  repl_sql = repmgr_component.send(:build_replication_user_sql, replication_user, effective_replication_password)
182
183
  executor.run_sql_on_backend(self, repl_sql, postgres_user: 'postgres', port: 5432, tuples_only: false,
183
- capture: false)
184
+ capture: false)
184
185
  end
185
186
 
186
187
  info 'Reloading PostgreSQL configuration to apply pg_hba.conf changes...'
@@ -202,14 +203,14 @@ module ActivePostgres
202
203
  cluster_show = nil
203
204
  5.times do |attempt|
204
205
  cluster_show = capture(:sudo, '-u', 'postgres', 'bash', '-lc',
205
- "repmgr cluster show -f /etc/repmgr.conf 2>&1", raise_on_non_zero_exit: false).to_s
206
+ 'repmgr cluster show -f /etc/repmgr.conf 2>&1', raise_on_non_zero_exit: false).to_s
206
207
 
207
208
  break if cluster_show.match?(/primary/i)
208
209
 
209
210
  sleep 2 if attempt < 4
210
211
  end
211
212
 
212
- unless cluster_show && cluster_show.match?(/primary/i)
213
+ unless cluster_show&.match?(/primary/i)
213
214
  # Fallback: verify via repmgr metadata in the repmgr database
214
215
  db_check = executor.run_sql_on_backend(self,
215
216
  'SELECT type FROM repmgr.nodes WHERE node_id = 1;',
@@ -255,13 +256,14 @@ module ActivePostgres
255
256
  if replication_user == repmgr_user && replication_password != repmgr_password
256
257
  raise Error, 'replication_user matches repmgr user but passwords differ. Use a distinct replication_user or the same password.'
257
258
  end
259
+
258
260
  effective_replication_password = replication_user == repmgr_user ? repmgr_password : replication_password
259
261
 
260
262
  ensure_primary_registered
261
263
  ensure_primary_replication_ready(repmgr_password, effective_replication_password)
262
264
 
263
265
  setup_pgpass_file(standby_host, repmgr_password, replication_password: effective_replication_password,
264
- primary_ip: primary_replication_host)
266
+ primary_ip: primary_replication_host)
265
267
 
266
268
  if standby_already_configured?(standby_host)
267
269
  puts ' Standby already configured, updating configs...'
@@ -418,9 +420,10 @@ module ActivePostgres
418
420
 
419
421
  def setup_inter_node_ssh
420
422
  dns_config = dns_failover_config
421
- key_path = dns_config && dns_config[:ssh_key_path] || '/var/lib/postgresql/.ssh/active_postgres_dns'
423
+ key_path = (dns_config && dns_config[:ssh_key_path]) || '/var/lib/postgresql/.ssh/active_postgres_dns'
422
424
  postgres_user = config.postgres_user
423
425
  all_hosts = config.all_hosts
426
+ replication_hosts = all_hosts.to_h { |host| [host, config.replication_host_for(host)] }
424
427
  pub_keys = {}
425
428
 
426
429
  all_hosts.each do |host|
@@ -430,18 +433,35 @@ module ActivePostgres
430
433
  all_hosts.each do |host|
431
434
  ssh_executor.execute_on_host(host) do
432
435
  other_keys = pub_keys.reject { |h, _| h == host }.values
433
- other_keys.each do |key|
434
- next if key.to_s.empty?
435
-
436
- upload! StringIO.new("#{key}\n"), '/tmp/pg_peer_key.pub'
437
- execute :sudo, '-u', postgres_user, 'bash', '-c',
438
- "grep -qxF -f /tmp/pg_peer_key.pub /var/lib/postgresql/.ssh/authorized_keys 2>/dev/null || cat /tmp/pg_peer_key.pub >> /var/lib/postgresql/.ssh/authorized_keys"
439
- execute :rm, '-f', '/tmp/pg_peer_key.pub'
436
+ authorized_keys_path = '/var/lib/postgresql/.ssh/authorized_keys'
437
+ existing_keys = if test(:sudo, 'test', '-f', authorized_keys_path)
438
+ capture(:sudo, 'cat', authorized_keys_path).lines.map(&:strip)
439
+ else
440
+ []
441
+ end
442
+ authorized_keys = (existing_keys + other_keys.map(&:to_s).map(&:strip)).reject(&:empty?).uniq.join("\n")
443
+ unless authorized_keys.empty?
444
+ upload! StringIO.new("#{authorized_keys}\n"), '/tmp/pg_authorized_keys'
445
+ execute :sudo, 'mv', '/tmp/pg_authorized_keys', authorized_keys_path
446
+ execute :sudo, 'chown', "#{postgres_user}:#{postgres_user}", authorized_keys_path
447
+ execute :sudo, 'chmod', '600', authorized_keys_path
440
448
  end
441
449
 
442
- peer_ips = all_hosts.reject { |h| h == host }.map { |h| config.replication_host_for(h) }
443
- scan_cmd = "ssh-keyscan #{peer_ips.join(' ')} >> /var/lib/postgresql/.ssh/known_hosts 2>/dev/null || true"
444
- execute :sudo, '-u', postgres_user, 'bash', '-c', scan_cmd
450
+ peer_ips = all_hosts.reject { |h| h == host }.map { |h| replication_hosts[h] }
451
+ known_hosts_path = '/var/lib/postgresql/.ssh/known_hosts'
452
+ existing_hosts = if test(:sudo, 'test', '-f', known_hosts_path)
453
+ capture(:sudo, 'cat', known_hosts_path).lines.map(&:strip)
454
+ else
455
+ []
456
+ end
457
+ scanned_hosts = capture(:sudo, '-u', postgres_user, 'ssh-keyscan', *peer_ips, raise_on_non_zero_exit: false)
458
+ known_hosts = (existing_hosts + scanned_hosts.lines.map(&:strip)).reject(&:empty?).uniq.join("\n")
459
+ unless known_hosts.empty?
460
+ upload! StringIO.new("#{known_hosts}\n"), '/tmp/pg_known_hosts'
461
+ execute :sudo, 'mv', '/tmp/pg_known_hosts', known_hosts_path
462
+ execute :sudo, 'chown', "#{postgres_user}:#{postgres_user}", known_hosts_path
463
+ execute :sudo, 'chmod', '600', known_hosts_path
464
+ end
445
465
  end
446
466
  end
447
467
  end
@@ -518,10 +538,21 @@ module ActivePostgres
518
538
  public_key = capture(:sudo, '-u', postgres_user, 'cat', "#{key_path}.pub").strip
519
539
 
520
540
  unless dns_servers.empty?
521
- execute :sudo, '-u', postgres_user, 'touch', '/var/lib/postgresql/.ssh/known_hosts'
522
- scan_cmd = "ssh-keyscan -H #{dns_servers.join(' ')} >> /var/lib/postgresql/.ssh/known_hosts 2>/dev/null || true"
523
- execute :sudo, '-u', postgres_user, 'bash', '-c', scan_cmd
524
- execute :sudo, '-u', postgres_user, 'chmod', '600', '/var/lib/postgresql/.ssh/known_hosts'
541
+ known_hosts_path = '/var/lib/postgresql/.ssh/known_hosts'
542
+ existing_hosts = if test(:sudo, 'test', '-f', known_hosts_path)
543
+ capture(:sudo, 'cat', known_hosts_path).lines.map(&:strip)
544
+ else
545
+ []
546
+ end
547
+ scanned_hosts = capture(:sudo, '-u', postgres_user, 'ssh-keyscan', '-H', *dns_servers,
548
+ raise_on_non_zero_exit: false)
549
+ known_hosts = (existing_hosts + scanned_hosts.lines.map(&:strip)).reject(&:empty?).uniq.join("\n")
550
+ unless known_hosts.empty?
551
+ upload! StringIO.new("#{known_hosts}\n"), '/tmp/pg_known_hosts'
552
+ execute :sudo, 'mv', '/tmp/pg_known_hosts', known_hosts_path
553
+ execute :sudo, 'chown', "#{postgres_user}:#{postgres_user}", known_hosts_path
554
+ execute :sudo, 'chmod', '600', known_hosts_path
555
+ end
525
556
  end
526
557
  end
527
558
 
@@ -542,8 +573,8 @@ module ActivePostgres
542
573
 
543
574
  upload! StringIO.new("#{key}\n"), '/tmp/active_postgres_dns_key.pub'
544
575
  execute :bash, '-c',
545
- "grep -qxF -f /tmp/active_postgres_dns_key.pub ~/.ssh/authorized_keys || " \
546
- "cat /tmp/active_postgres_dns_key.pub >> ~/.ssh/authorized_keys"
576
+ 'grep -qxF -f /tmp/active_postgres_dns_key.pub ~/.ssh/authorized_keys || ' \
577
+ 'cat /tmp/active_postgres_dns_key.pub >> ~/.ssh/authorized_keys'
547
578
  execute :rm, '-f', '/tmp/active_postgres_dns_key.pub'
548
579
  end
549
580
  end
@@ -560,11 +591,11 @@ module ActivePostgres
560
591
 
561
592
  domains = normalize_dns_domains(dns_config)
562
593
  primary_records = normalize_dns_records(dns_config[:primary_records] || dns_config[:primary_record],
563
- default_prefix: 'db-primary',
564
- domains: domains)
594
+ default_prefix: 'db-primary',
595
+ domains: domains)
565
596
  replica_records = normalize_dns_records(dns_config[:replica_records] || dns_config[:replica_record],
566
- default_prefix: 'db-replica',
567
- domains: domains)
597
+ default_prefix: 'db-replica',
598
+ domains: domains)
568
599
 
569
600
  _ = primary_records
570
601
  _ = replica_records
@@ -732,10 +763,10 @@ module ActivePostgres
732
763
  # Check replication status
733
764
  begin
734
765
  rep_status = executor.run_sql_on_backend(self,
735
- 'SELECT pg_is_in_recovery();',
736
- postgres_user: postgres_user,
737
- tuples_only: true,
738
- capture: true).to_s
766
+ 'SELECT pg_is_in_recovery();',
767
+ postgres_user: postgres_user,
768
+ tuples_only: true,
769
+ capture: true).to_s
739
770
  if rep_status.include?('t')
740
771
  info '✓ Standby is in recovery mode (receiving replication)'
741
772
  else
@@ -750,10 +781,10 @@ module ActivePostgres
750
781
  # Check lag
751
782
  begin
752
783
  lag_result = executor.run_sql_on_backend(self,
753
- 'SELECT EXTRACT(EPOCH FROM (now() - pg_last_xact_replay_timestamp()))::int AS lag;',
754
- postgres_user: postgres_user,
755
- tuples_only: true,
756
- capture: true).to_s.strip
784
+ 'SELECT EXTRACT(EPOCH FROM (now() - pg_last_xact_replay_timestamp()))::int AS lag;',
785
+ postgres_user: postgres_user,
786
+ tuples_only: true,
787
+ capture: true).to_s.strip
757
788
  info "Replication lag: #{lag_result}"
758
789
  rescue StandardError => e
759
790
  error "Failed to get replication lag: #{e.message}"
@@ -785,7 +816,7 @@ module ActivePostgres
785
816
  # Create .pgpass file for postgres user to avoid password exposure in logs
786
817
  # Format: hostname:port:database:username:password
787
818
  pgpass_content = build_pgpass_content(host, repmgr_password, replication_password: replication_password,
788
- primary_ip: primary_ip)
819
+ primary_ip: primary_ip)
789
820
 
790
821
  ssh_executor.execute_on_host(host) do
791
822
  # Create .pgpass in postgres user's home directory
@@ -854,8 +885,13 @@ module ActivePostgres
854
885
  def escape_pgpass_value(value)
855
886
  return '' if value.nil?
856
887
 
857
- # Must escape backslashes first, then colons (order matters!)
858
- value.to_s.gsub('\\', '\\\\\\\\').gsub(':', '\\:')
888
+ value.to_s.each_char.map do |char|
889
+ case char
890
+ when '\\' then '\\\\'
891
+ when ':' then '\\:'
892
+ else char
893
+ end
894
+ end.join
859
895
  end
860
896
 
861
897
  def build_primary_conninfo(standby_label)
@@ -935,12 +971,12 @@ module ActivePostgres
935
971
  ssh_executor.execute_on_host(host) do
936
972
  repmgr_sql = repmgr_component.send(:build_repmgr_setup_sql, repmgr_user, repmgr_db, repmgr_password)
937
973
  executor.run_sql_on_backend(self, repmgr_sql, postgres_user: 'postgres', port: 5432, tuples_only: false,
938
- capture: false)
974
+ capture: false)
939
975
 
940
976
  if replication_user != repmgr_user
941
977
  repl_sql = repmgr_component.send(:build_replication_user_sql, replication_user, effective_replication_password)
942
978
  executor.run_sql_on_backend(self, repl_sql, postgres_user: 'postgres', port: 5432, tuples_only: false,
943
- capture: false)
979
+ capture: false)
944
980
  end
945
981
 
946
982
  info '✓ Primary replication user is ready'
@@ -125,11 +125,10 @@ module ActivePostgres
125
125
  # Validate required secrets if components are enabled
126
126
  raise Error, 'Missing replication_password secret' if component_enabled?(:repmgr) && !secrets_config['replication_password']
127
127
  raise Error, 'Missing monitoring_password secret' if component_enabled?(:monitoring) && !secrets_config['monitoring_password']
128
+
128
129
  if component_enabled?(:monitoring)
129
130
  grafana_config = component_config(:monitoring)[:grafana] || {}
130
- if grafana_config[:enabled] && !secrets_config['grafana_admin_password']
131
- raise Error, 'Missing grafana_admin_password secret'
132
- end
131
+ raise Error, 'Missing grafana_admin_password secret' if grafana_config[:enabled] && !secrets_config['grafana_admin_password']
133
132
  if grafana_config[:enabled] && grafana_config[:host].to_s.strip.empty?
134
133
  raise Error, 'monitoring.grafana.host is required when grafana is enabled'
135
134
  end
@@ -150,27 +149,27 @@ module ActivePostgres
150
149
  end
151
150
  end
152
151
 
153
- if component_enabled?(:repmgr)
154
- dns_failover = component_config(:repmgr)[:dns_failover]
155
- if dns_failover && dns_failover[:enabled]
156
- domains = Array(dns_failover[:domains] || dns_failover[:domain]).map(&:to_s).map(&:strip).reject(&:empty?)
157
- servers = Array(dns_failover[:dns_servers])
158
- provider = (dns_failover[:provider] || 'dnsmasq').to_s.strip
159
-
160
- raise Error, 'dns_failover.domain or dns_failover.domains is required when enabled' if domains.empty?
161
- raise Error, 'dns_failover.dns_servers is required when enabled' if servers.empty?
162
- raise Error, "Unsupported dns_failover provider '#{provider}'" unless provider == 'dnsmasq'
163
-
164
- servers.each do |server|
165
- next unless server.is_a?(Hash)
166
-
167
- ssh_host = server['ssh_host'] || server[:ssh_host] || server['host'] || server[:host]
168
- private_ip = server['private_ip'] || server[:private_ip] || server['ip'] || server[:ip]
169
- raise Error, 'dns_failover.dns_servers entries must include host/ssh_host or private_ip' if
170
- (ssh_host.nil? || ssh_host.to_s.strip.empty?) && (private_ip.nil? || private_ip.to_s.strip.empty?)
171
- end
152
+ if component_enabled?(:repmgr)
153
+ dns_failover = component_config(:repmgr)[:dns_failover]
154
+ if dns_failover && dns_failover[:enabled]
155
+ domains = Array(dns_failover[:domains] || dns_failover[:domain]).map(&:to_s).map(&:strip).reject(&:empty?)
156
+ servers = Array(dns_failover[:dns_servers])
157
+ provider = (dns_failover[:provider] || 'dnsmasq').to_s.strip
158
+
159
+ raise Error, 'dns_failover.domain or dns_failover.domains is required when enabled' if domains.empty?
160
+ raise Error, 'dns_failover.dns_servers is required when enabled' if servers.empty?
161
+ raise Error, "Unsupported dns_failover provider '#{provider}'" unless provider == 'dnsmasq'
162
+
163
+ servers.each do |server|
164
+ next unless server.is_a?(Hash)
165
+
166
+ ssh_host = server['ssh_host'] || server[:ssh_host] || server['host'] || server[:host]
167
+ private_ip = server['private_ip'] || server[:private_ip] || server['ip'] || server[:ip]
168
+ raise Error, 'dns_failover.dns_servers entries must include host/ssh_host or private_ip' if
169
+ (ssh_host.nil? || ssh_host.to_s.strip.empty?) && (private_ip.nil? || private_ip.to_s.strip.empty?)
172
170
  end
173
171
  end
172
+ end
174
173
 
175
174
  true
176
175
  end
@@ -62,19 +62,13 @@ module ActivePostgres
62
62
  return nil if value.nil?
63
63
 
64
64
  # Handle Rails credentials
65
- if value.start_with?('rails_credentials:')
66
- return resolve_rails_credentials(value)
67
- end
65
+ return resolve_rails_credentials(value) if value.start_with?('rails_credentials:')
68
66
 
69
67
  # Handle environment variables
70
- if value.start_with?('$')
71
- return ENV[value[1..]]
72
- end
68
+ return ENV.fetch(value[1..], nil) if value.start_with?('$')
73
69
 
74
70
  # Handle shell commands
75
- if value.start_with?('$(') && value.end_with?(')')
76
- return `#{value[2..-2]}`.strip
77
- end
71
+ return `#{value[2..-2]}`.strip if value.start_with?('$(') && value.end_with?(')')
78
72
 
79
73
  value
80
74
  end
@@ -127,24 +127,24 @@ module ActivePostgres
127
127
 
128
128
  # Identify error type from error message
129
129
  def identify_error_type(message)
130
- case message.downcase
131
- when /ssh|connection refused|network unreachable/
132
- :ssh_connection
133
- when /private network|vpn|wireguard network/
134
- :private_network_connectivity
135
- when /postgresql.*not.*start|cluster.*not.*running/
136
- :postgresql_not_starting
137
- when /repmgr.*clone|data directory/
138
- :repmgr_clone_failed
139
- when /repmgr.*register|unable to connect to.*primary/
140
- :repmgr_register_failed
141
- when /ssl|certificate|tls/
142
- :ssl_certificate_error
143
- when /no space|disk full/
144
- :disk_space_error
145
- when /authentication|password|pg_hba/
146
- :authentication_failed
147
- end
130
+ normalized = message.to_s.downcase
131
+
132
+ return :ssh_connection if contains_any?(normalized, 'ssh', 'connection refused', 'network unreachable')
133
+ return :private_network_connectivity if contains_any?(normalized, 'private network', 'vpn', 'wireguard network')
134
+ return :postgresql_not_starting if normalized.include?('postgresql') && normalized.include?('not') && normalized.include?('start')
135
+ return :postgresql_not_starting if normalized.include?('cluster') && normalized.include?('not') && normalized.include?('running')
136
+ return :repmgr_clone_failed if normalized.include?('repmgr') && normalized.include?('clone')
137
+ return :repmgr_clone_failed if normalized.include?('data directory')
138
+ return :repmgr_register_failed if normalized.include?('repmgr') && normalized.include?('register')
139
+ return :repmgr_register_failed if normalized.include?('unable to connect to') && normalized.include?('primary')
140
+ return :ssl_certificate_error if contains_any?(normalized, 'ssl', 'certificate', 'tls')
141
+ return :disk_space_error if contains_any?(normalized, 'no space', 'disk full')
142
+
143
+ :authentication_failed if contains_any?(normalized, 'authentication', 'password', 'pg_hba')
144
+ end
145
+
146
+ def contains_any?(value, *needles)
147
+ needles.any? { |needle| value.include?(needle) }
148
148
  end
149
149
 
150
150
  # Show troubleshooting guide for a specific error type
@@ -12,14 +12,6 @@ module ActivePostgres
12
12
  @executor
13
13
  end
14
14
 
15
- private def create_executor
16
- if use_ssh_executor?
17
- SSHExecutor.new(config, quiet: true)
18
- else
19
- DirectExecutor.new(config, quiet: true)
20
- end
21
- end
22
-
23
15
  def use_ssh_executor?
24
16
  mode = ENV['ACTIVE_POSTGRES_STATUS_MODE'].to_s.strip.downcase
25
17
  return true if mode == 'ssh'
@@ -131,6 +123,14 @@ module ActivePostgres
131
123
 
132
124
  private
133
125
 
126
+ def create_executor
127
+ if use_ssh_executor?
128
+ SSHExecutor.new(config, quiet: true)
129
+ else
130
+ DirectExecutor.new(config, quiet: true)
131
+ end
132
+ end
133
+
134
134
  def collect_node_status
135
135
  nodes = []
136
136
 
@@ -184,9 +184,7 @@ module ActivePostgres
184
184
  lag: [3, nodes.map { |n| n[:lag].to_s.length }.max].max
185
185
  }
186
186
 
187
- if config.component_enabled?(:pgbouncer)
188
- cols[:pgbouncer] = [9, nodes.map { |n| n[:pgbouncer].to_s.length }.max].max
189
- end
187
+ cols[:pgbouncer] = [9, nodes.map { |n| n[:pgbouncer].to_s.length }.max].max if config.component_enabled?(:pgbouncer)
190
188
 
191
189
  cols
192
190
  end
@@ -194,7 +192,7 @@ module ActivePostgres
194
192
  def print_table_header(cols)
195
193
  fmt = "%-#{cols[:role]}s %-#{cols[:host]}s %-#{cols[:private_ip]}s " \
196
194
  "%-#{cols[:label]}s %-#{cols[:status]}s %#{cols[:conn]}s %#{cols[:lag]}s"
197
- headers = %w[Role Host Private\ IP Label Status Conn Lag]
195
+ headers = ['Role', 'Host', 'Private IP', 'Label', 'Status', 'Conn', 'Lag']
198
196
 
199
197
  if cols[:pgbouncer]
200
198
  fmt += " %-#{cols[:pgbouncer]}s"
@@ -61,14 +61,14 @@ module ActivePostgres
61
61
  label = config.node_label_for(host)
62
62
  stats = fetch_system_stats(host, paths)
63
63
  if stats.nil?
64
- puts " #{host}#{label ? " (#{label})" : ''}: unavailable"
64
+ puts " #{host}#{" (#{label})" if label}: unavailable"
65
65
  next
66
66
  end
67
67
 
68
68
  mem_used_kb = stats[:mem_total_kb] - stats[:mem_avail_kb]
69
69
  mem_pct = stats[:mem_total_kb].positive? ? (mem_used_kb.to_f / stats[:mem_total_kb] * 100).round : 0
70
70
 
71
- puts " #{host}#{label ? " (#{label})" : ''}: load #{stats[:loadavg]} | cpu #{stats[:cpu]}% | " \
71
+ puts " #{host}#{" (#{label})" if label}: load #{stats[:loadavg]} | cpu #{stats[:cpu]}% | " \
72
72
  "mem #{format_kb(mem_used_kb)}/#{format_kb(stats[:mem_total_kb])} (#{mem_pct}%)"
73
73
 
74
74
  stats[:disks].each do |disk|
@@ -227,7 +227,7 @@ module ActivePostgres
227
227
  with_timeout("system stats #{host}") do
228
228
  ssh_executor = executor
229
229
  safe_paths = paths.map { |p| Shellwords.escape(p) }.join(' ')
230
- script = <<~'BASH'
230
+ script = <<~BASH
231
231
  set -e
232
232
  loadavg=$(cut -d' ' -f1-3 /proc/loadavg)
233
233
 
@@ -256,7 +256,7 @@ module ActivePostgres
256
256
 
257
257
  ssh_executor.execute_on_host(host) do
258
258
  output = capture(:bash, '-lc', "#{script}\n df -kP #{safe_paths} 2>/dev/null | tail -n +2 | " \
259
- "awk '{print \"disk=\" $6 \"|\" $2 \"|\" $3 \"|\" $4 \"|\" $5}'",
259
+ "awk '{print \"disk=\" $6 \"|\" $2 \"|\" $3 \"|\" $4 \"|\" $5}'",
260
260
  raise_on_non_zero_exit: false).to_s
261
261
  end
262
262
  end
@@ -299,12 +299,12 @@ module ActivePostgres
299
299
  stats
300
300
  end
301
301
 
302
- def format_kb(kb)
303
- kb = kb.to_f
304
- gb = kb / 1024 / 1024
302
+ def format_kb(kilobytes)
303
+ kilobytes = kilobytes.to_f
304
+ gb = kilobytes / 1024 / 1024
305
305
  return format('%.1fG', gb) if gb >= 1
306
306
 
307
- mb = kb / 1024
307
+ mb = kilobytes / 1024
308
308
  format('%.0fM', mb)
309
309
  end
310
310
 
@@ -342,8 +342,8 @@ module ActivePostgres
342
342
  value.positive? ? value : DEFAULT_TIMEOUT
343
343
  end
344
344
 
345
- def with_timeout(label)
346
- Timeout.timeout(overview_timeout) { yield }
345
+ def with_timeout(label, &)
346
+ Timeout.timeout(overview_timeout, &)
347
347
  rescue Timeout::Error
348
348
  raise StandardError, "#{label} timed out after #{overview_timeout}s"
349
349
  end
@@ -20,8 +20,8 @@ module ActivePostgres
20
20
  end
21
21
 
22
22
  def resolve_all
23
- config.secrets_config.keys.each_with_object({}) do |key, result|
24
- result[key] = resolve(key)
23
+ config.secrets_config.keys.to_h do |key|
24
+ [key, resolve(key)]
25
25
  end
26
26
  end
27
27
 
@@ -47,8 +47,9 @@ module ActivePostgres
47
47
 
48
48
  resolve_all.each do |key, value|
49
49
  file_path = File.join(directory, key)
50
- File.write(file_path, value)
51
- File.chmod(0o600, file_path)
50
+ File.open(file_path, File::WRONLY | File::CREAT | File::TRUNC, 0o600) do |file|
51
+ file.write(value)
52
+ end
52
53
  puts "Cached #{key} to #{file_path}"
53
54
  end
54
55
 
@@ -101,9 +102,7 @@ module ActivePostgres
101
102
 
102
103
  # Try loading just the Rails application (without full initialization)
103
104
  # This makes Rails.application available for credential access
104
- if File.exist?('./config/application.rb')
105
- require './config/application'
106
- end
105
+ require './config/application' if File.exist?('./config/application.rb')
107
106
  rescue StandardError
108
107
  # Rails boot failed — CLI running outside a Rails project
109
108
  nil
@@ -215,11 +215,11 @@ module ActivePostgres
215
215
  execute_on_host(host) do
216
216
  backend = self
217
217
  result = executor.run_sql_on_backend(backend, sql,
218
- postgres_user: postgres_user,
219
- port: port,
220
- database: database,
221
- tuples_only: tuples_only,
222
- capture: capture)
218
+ postgres_user: postgres_user,
219
+ port: port,
220
+ database: database,
221
+ tuples_only: tuples_only,
222
+ capture: capture)
223
223
  end
224
224
  result
225
225
  end
@@ -1,3 +1,3 @@
1
1
  module ActivePostgres
2
- VERSION = '0.9.5'.freeze
2
+ VERSION = '0.9.6'.freeze
3
3
  end
@@ -287,39 +287,39 @@ namespace :postgres do
287
287
  puts "\nTip: set RAILS_ENV=production for production targets"
288
288
 
289
289
  puts "\nSetup & Maintenance"
290
- puts " rake postgres:setup # Deploy HA cluster (CLEAN=true for fresh install)"
291
- puts " rake postgres:purge # Destroy cluster (DESTRUCTIVE)"
292
- puts " rake postgres:setup:core # PostgreSQL config (postgresql.conf, pg_hba.conf)"
293
- puts " rake postgres:setup:repmgr # HA + failover (repmgr)"
294
- puts " rake postgres:setup:pgbouncer # PgBouncer pooling"
295
- puts " rake postgres:setup:pgbackrest # Backups (pgBackRest)"
296
- puts " rake postgres:setup:monitoring # postgres_exporter"
297
- puts " rake postgres:setup:ssl # SSL certs"
290
+ puts ' rake postgres:setup # Deploy HA cluster (CLEAN=true for fresh install)'
291
+ puts ' rake postgres:purge # Destroy cluster (DESTRUCTIVE)'
292
+ puts ' rake postgres:setup:core # PostgreSQL config (postgresql.conf, pg_hba.conf)'
293
+ puts ' rake postgres:setup:repmgr # HA + failover (repmgr)'
294
+ puts ' rake postgres:setup:pgbouncer # PgBouncer pooling'
295
+ puts ' rake postgres:setup:pgbackrest # Backups (pgBackRest)'
296
+ puts ' rake postgres:setup:monitoring # postgres_exporter'
297
+ puts ' rake postgres:setup:ssl # SSL certs'
298
298
 
299
299
  puts "\nStatus & Health"
300
- puts " rake postgres:status # Cluster status (SSH by default)"
301
- puts " rake postgres:overview # Control tower overview"
302
- puts " rake postgres:nodes # Topology view"
303
- puts " rake postgres:verify # Comprehensive checklist"
304
- puts " ACTIVE_POSTGRES_STATUS_MODE=ssh|direct rake postgres:status"
300
+ puts ' rake postgres:status # Cluster status (SSH by default)'
301
+ puts ' rake postgres:overview # Control tower overview'
302
+ puts ' rake postgres:nodes # Topology view'
303
+ puts ' rake postgres:verify # Comprehensive checklist'
304
+ puts ' ACTIVE_POSTGRES_STATUS_MODE=ssh|direct rake postgres:status'
305
305
 
306
306
  puts "\nBackups"
307
- puts " rake postgres:backup:full # Full backup"
308
- puts " rake postgres:backup:incremental # Incremental backup"
309
- puts " rake postgres:backup:list # List backups"
310
- puts " rake postgres:backup:restore[ID] # Restore backup set"
311
- puts " rake postgres:backup:restore_at[\"YYYY-MM-DD HH:MM:SS\",promote] # PITR"
307
+ puts ' rake postgres:backup:full # Full backup'
308
+ puts ' rake postgres:backup:incremental # Incremental backup'
309
+ puts ' rake postgres:backup:list # List backups'
310
+ puts ' rake postgres:backup:restore[ID] # Restore backup set'
311
+ puts ' rake postgres:backup:restore_at["YYYY-MM-DD HH:MM:SS",promote] # PITR'
312
312
 
313
313
  puts "\nMigrations"
314
- puts " rake postgres:migrate # Run migrations on primary only"
314
+ puts ' rake postgres:migrate # Run migrations on primary only'
315
315
 
316
316
  puts "\nPgBouncer"
317
- puts " rake postgres:pgbouncer:update_userlist[users] # Update userlist"
318
- puts " rake postgres:pgbouncer:stats # Service status + stats"
317
+ puts ' rake postgres:pgbouncer:update_userlist[users] # Update userlist'
318
+ puts ' rake postgres:pgbouncer:stats # Service status + stats'
319
319
 
320
320
  puts "\nTests"
321
- puts " rake postgres:test:replication[rows] # Replication stress test"
322
- puts " rake postgres:test:pgbouncer[connections] # PgBouncer load test"
321
+ puts ' rake postgres:test:replication[rows] # Replication stress test'
322
+ puts ' rake postgres:test:pgbouncer[connections] # PgBouncer load test'
323
323
 
324
324
  puts
325
325
  end
@@ -380,7 +380,7 @@ namespace :postgres do
380
380
  end
381
381
 
382
382
  desc 'Restore to a point in time (PITR)'
383
- task :restore_at, [:target_time, :target_action] => :environment do |_t, args|
383
+ task :restore_at, %i[target_time target_action] => :environment do |_t, args|
384
384
  require 'active_postgres'
385
385
 
386
386
  unless args[:target_time]
@@ -607,8 +607,8 @@ namespace :postgres do
607
607
  nil
608
608
  end
609
609
  if cert_issuer
610
- issuer_o = cert_issuer.match(/O\s*=\s*"?([^",\/]+)"?/)&.captures&.first
611
- issuer_cn = cert_issuer.match(/CN\s*=\s*([^,\/]+)/)&.captures&.first
610
+ issuer_o = cert_issuer.match(%r{O\s*=\s*"?([^",/]+)"?})&.captures&.first
611
+ issuer_cn = cert_issuer.match(%r{CN\s*=\s*([^,/]+)})&.captures&.first
612
612
  issuer_name = issuer_o || issuer_cn || cert_issuer.sub('issuer=', '')
613
613
  info " Issuer: #{issuer_name.strip}"
614
614
  end
@@ -52,6 +52,11 @@ archive_command = '<%= pg_config[:archive_command] %>'
52
52
  <% elsif config.component_enabled?(:pgbackrest) %>
53
53
  archive_command = 'pgbackrest --stanza=main archive-push %p'
54
54
  <% end %>
55
+ <% if pg_config[:restore_command] %>
56
+ restore_command = '<%= pg_config[:restore_command] %>'
57
+ <% elsif config.component_enabled?(:pgbackrest) %>
58
+ restore_command = 'pgbackrest --pg1-path=/var/lib/postgresql/<%= config.version %>/main --stanza=main archive-get %f "%p"'
59
+ <% end %>
55
60
  <% if pg_config[:shared_memory_type] %>
56
61
  shared_memory_type = <%= pg_config[:shared_memory_type] %>
57
62
  <% end %>
@@ -17,13 +17,13 @@ REPMGR_DB=$(grep -oP "dbname=\K[^ ']+" "$REPMGR_CONF" | head -1)
17
17
  REPMGR_USER=$(grep -oP "user=\K[^ ']+" "$REPMGR_CONF" 2>/dev/null | head -1)
18
18
  REPMGR_USER="${REPMGR_USER:-repmgr}"
19
19
 
20
- cluster_data=$(psql -h /var/run/postgresql -d "$REPMGR_DB" -tAF',' -c "SELECT type, conninfo FROM repmgr.nodes WHERE active = true" 2>/dev/null || true)
20
+ cluster_data=$(psql -h /var/run/postgresql -d "$REPMGR_DB" -tAF',' -c "SELECT type, conninfo FROM repmgr.nodes WHERE active = true ORDER BY node_id" 2>/dev/null || true)
21
21
  if [[ -z "$cluster_data" ]]; then
22
22
  exit 0
23
23
  fi
24
24
 
25
25
  primary_host=$(printf "%s\n" "$cluster_data" | awk -F',' '$1 == "primary" {print $2; exit}' | sed -n 's/.*host=\([^ ]*\).*/\1/p')
26
- standby_hosts=$(printf "%s\n" "$cluster_data" | awk -F',' '$1 == "standby" {print $2}' | sed -n 's/.*host=\([^ ]*\).*/\1/p' | sort -u)
26
+ standby_hosts=$(printf "%s\n" "$cluster_data" | awk -F',' '$1 == "standby" {print $2; exit}' | sed -n 's/.*host=\([^ ]*\).*/\1/p')
27
27
 
28
28
  if [[ -z "$primary_host" ]]; then
29
29
  exit 0
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.5
4
+ version: 0.9.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - BoringCache
@@ -29,35 +29,35 @@ dependencies:
29
29
  requirements:
30
30
  - - "~>"
31
31
  - !ruby/object:Gem::Version
32
- version: '1.3'
32
+ version: '1.4'
33
33
  type: :runtime
34
34
  prerelease: false
35
35
  version_requirements: !ruby/object:Gem::Requirement
36
36
  requirements:
37
37
  - - "~>"
38
38
  - !ruby/object:Gem::Version
39
- version: '1.3'
39
+ version: '1.4'
40
40
  - !ruby/object:Gem::Dependency
41
41
  name: pg
42
42
  requirement: !ruby/object:Gem::Requirement
43
43
  requirements:
44
44
  - - "~>"
45
45
  - !ruby/object:Gem::Version
46
- version: '1.0'
46
+ version: '1.6'
47
47
  type: :runtime
48
48
  prerelease: false
49
49
  version_requirements: !ruby/object:Gem::Requirement
50
50
  requirements:
51
51
  - - "~>"
52
52
  - !ruby/object:Gem::Version
53
- version: '1.0'
53
+ version: '1.6'
54
54
  - !ruby/object:Gem::Dependency
55
55
  name: railties
56
56
  requirement: !ruby/object:Gem::Requirement
57
57
  requirements:
58
58
  - - ">="
59
59
  - !ruby/object:Gem::Version
60
- version: '6.0'
60
+ version: '8.1'
61
61
  - - "<"
62
62
  - !ruby/object:Gem::Version
63
63
  version: '9.0'
@@ -67,7 +67,7 @@ dependencies:
67
67
  requirements:
68
68
  - - ">="
69
69
  - !ruby/object:Gem::Version
70
- version: '6.0'
70
+ version: '8.1'
71
71
  - - "<"
72
72
  - !ruby/object:Gem::Version
73
73
  version: '9.0'
@@ -77,70 +77,28 @@ dependencies:
77
77
  requirements:
78
78
  - - "~>"
79
79
  - !ruby/object:Gem::Version
80
- version: '1.21'
80
+ version: '1.25'
81
81
  type: :runtime
82
82
  prerelease: false
83
83
  version_requirements: !ruby/object:Gem::Requirement
84
84
  requirements:
85
85
  - - "~>"
86
86
  - !ruby/object:Gem::Version
87
- version: '1.21'
87
+ version: '1.25'
88
88
  - !ruby/object:Gem::Dependency
89
89
  name: thor
90
90
  requirement: !ruby/object:Gem::Requirement
91
91
  requirements:
92
92
  - - "~>"
93
93
  - !ruby/object:Gem::Version
94
- version: '1.3'
94
+ version: '1.5'
95
95
  type: :runtime
96
96
  prerelease: false
97
97
  version_requirements: !ruby/object:Gem::Requirement
98
98
  requirements:
99
99
  - - "~>"
100
100
  - !ruby/object:Gem::Version
101
- version: '1.3'
102
- - !ruby/object:Gem::Dependency
103
- name: minitest
104
- requirement: !ruby/object:Gem::Requirement
105
- requirements:
106
- - - "~>"
107
- - !ruby/object:Gem::Version
108
- version: '5.0'
109
- type: :development
110
- prerelease: false
111
- version_requirements: !ruby/object:Gem::Requirement
112
- requirements:
113
- - - "~>"
114
- - !ruby/object:Gem::Version
115
- version: '5.0'
116
- - !ruby/object:Gem::Dependency
117
- name: rubocop
118
- requirement: !ruby/object:Gem::Requirement
119
- requirements:
120
- - - "~>"
121
- - !ruby/object:Gem::Version
122
- version: '1.50'
123
- type: :development
124
- prerelease: false
125
- version_requirements: !ruby/object:Gem::Requirement
126
- requirements:
127
- - - "~>"
128
- - !ruby/object:Gem::Version
129
- version: '1.50'
130
- - !ruby/object:Gem::Dependency
131
- name: simplecov
132
- requirement: !ruby/object:Gem::Requirement
133
- requirements:
134
- - - "~>"
135
- - !ruby/object:Gem::Version
136
- version: '0.22'
137
- type: :development
138
- prerelease: false
139
- version_requirements: !ruby/object:Gem::Requirement
140
- requirements:
141
- - - "~>"
142
- - !ruby/object:Gem::Version
143
- version: '0.22'
101
+ version: '1.5'
144
102
  description: Production-grade PostgreSQL HA with deep Rails integration. Automated
145
103
  deployment, replication, failover, and monitoring.
146
104
  email:
@@ -150,8 +108,10 @@ executables:
150
108
  extensions: []
151
109
  extra_rdoc_files: []
152
110
  files:
111
+ - CHANGELOG.md
153
112
  - LICENSE
154
113
  - README.md
114
+ - SECURITY.md
155
115
  - exe/activepostgres
156
116
  - lib/active_postgres.rb
157
117
  - lib/active_postgres/cli.rb
@@ -209,10 +169,10 @@ homepage: https://github.com/boringcache/active_postgres
209
169
  licenses:
210
170
  - MIT
211
171
  metadata:
212
- homepage_uri: https://github.com/boringcache/active_postgres
213
172
  source_code_uri: https://github.com/boringcache/active_postgres
214
173
  documentation_uri: https://github.com/boringcache/active_postgres/blob/main/README.md
215
174
  changelog_uri: https://github.com/boringcache/active_postgres/blob/main/CHANGELOG.md
175
+ bug_tracker_uri: https://github.com/boringcache/active_postgres/issues
216
176
  rubygems_mfa_required: 'true'
217
177
  rdoc_options: []
218
178
  require_paths:
@@ -221,14 +181,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
221
181
  requirements:
222
182
  - - ">="
223
183
  - !ruby/object:Gem::Version
224
- version: 3.3.0
184
+ version: 4.0.0
225
185
  required_rubygems_version: !ruby/object:Gem::Requirement
226
186
  requirements:
227
187
  - - ">="
228
188
  - !ruby/object:Gem::Version
229
189
  version: '0'
230
190
  requirements: []
231
- rubygems_version: 3.6.9
191
+ rubygems_version: 4.0.16
232
192
  specification_version: 4
233
193
  summary: PostgreSQL High Availability for Rails, made simple
234
194
  test_files: []