active_postgres 0.9.5 → 0.9.7

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: 2f7bf86efa4e32bcb1543e317bd6219983f9f42eb0ed0bf8787ff75b6a573a48
4
+ data.tar.gz: 8a2b2f0ec44bdfd94552dfa0fd84145b64ded472b4f421f216c86af71e2d859e
5
5
  SHA512:
6
- metadata.gz: 81cc77508c361ab128d840dbaac52ae9f3fc50b431fa971aaede91fc573ed9186521a53f8098e038515b6c96c82141100fd7ce797b0eae5c4b2840bd01330212
7
- data.tar.gz: bcdeb64f655cbc0e702f5b70fc3c749daa103ac74945a9432627aeff6926de9b1974fa52547b32e1d99acec687d30b252473a15a75ffadd299e6bc951e9e2bf2
6
+ metadata.gz: 1d4a26f7284e14c84bcaa4d1e982da9162a25a098b15f9423b4923e7b7a9ab007112620761e5184a1da514f40495edd00647e8c2a269bbbb0511e2611e44118f
7
+ data.tar.gz: 4dfb7b6e19345e3324d38a798a82bdfa111001cb94736348aa88ba6146f9c3a6bc47fed8977039faefc271d412529f61995ad36f42be93b96a8789437187423e
data/CHANGELOG.md ADDED
@@ -0,0 +1,40 @@
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.7 - 2026-09-01
11
+
12
+ ### Added
13
+
14
+ - Support strict operator SSH through declared jump hosts.
15
+ - Allow a standby to override repmgr failover and priority policy.
16
+ - Allow a standby to be seeded from the latest valid pgBackRest backup.
17
+ - Make deployment-flow `--dry-run` stop before confirmation or mutation.
18
+ - Fail standby setup before mutation when required credentials do not resolve.
19
+ - Scope standby rollback to the requested host and keep existing cluster nodes intact.
20
+ - Preserve pgBackRest recovery options as one shell-safe argument during remote restore.
21
+ - Preserve the complete pgBackRest command as the single `bash -lc` command string.
22
+ - Run component rollback in local Ruby context instead of an SSHKit backend scope.
23
+ - Explicitly stop SysV-backed repmgrd before disabling it on manual-failover standbys.
24
+ - Support opt-in bundled block backups for large, frequently changed PostgreSQL relations.
25
+ - Support independent full, differential, and incremental pgBackRest schedules.
26
+
27
+ ## 0.9.6 - 2026-08-20
28
+
29
+ ### Changed
30
+
31
+ - Require Ruby 4.0 or newer and Rails 8.1 or newer for Rails integration.
32
+ - Align runtime and development dependencies with the maintained toolchain.
33
+ - Add blocking test, lint, dependency-audit, and package-build CI checks.
34
+ - Add automated Bundler and GitHub Actions dependency updates.
35
+ - Document network, credential, failover, and recovery responsibilities.
36
+
37
+ ## 0.9.5
38
+
39
+ - Current published release. See the repository history for changes included in
40
+ 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
 
@@ -31,6 +35,11 @@ production:
31
35
  version: 18
32
36
  user: ubuntu
33
37
  ssh_key: ~/.ssh/id_rsa
38
+ ssh_known_hosts_file: config/ssh_known_hosts
39
+
40
+ jump_hosts:
41
+ app-oci:
42
+ host: 129.213.194.40
34
43
 
35
44
  primary:
36
45
  host: 34.12.234.81 # Public IP for SSH
@@ -39,6 +48,13 @@ production:
39
48
  standby:
40
49
  - host: 52.23.45.67
41
50
  private_ip: 10.8.0.11
51
+ - host: 100.63.4.227
52
+ private_ip: 10.8.0.12
53
+ jump_host: app-oci
54
+ seed_method: pgbackrest
55
+ repmgr:
56
+ auto_failover: false
57
+ priority: 0
42
58
 
43
59
  components:
44
60
  repmgr: {enabled: true}
@@ -52,6 +68,14 @@ production:
52
68
  app_password: $POSTGRES_APP_PASSWORD
53
69
  ```
54
70
 
71
+ `jump_host` keeps the operator SSH key on the workstation and proxies the
72
+ connection through a declared host. A standby-level `repmgr` block overrides
73
+ the cluster defaults, so a cross-provider disaster-recovery replica can remain
74
+ manual-promotion while the ordinary standby retains automatic failover.
75
+ `seed_method: pgbackrest` restores the latest valid backup with standby recovery
76
+ settings before repmgr registration, avoiding a full base-backup stream from
77
+ the primary. The `pgbackrest` component must be enabled.
78
+
55
79
  Add credentials (`rails credentials:edit`):
56
80
 
57
81
  ```yaml
@@ -175,8 +199,13 @@ components:
175
199
  pgbackrest:
176
200
  retention_full: 7
177
201
  retention_archive: 14
178
- # You can schedule full + incremental separately:
179
- # schedule_full: "0 2 * * *"
202
+ # Reduce incremental backup churn for large, frequently changed relations.
203
+ # Block backups require repository bundling.
204
+ repo_bundle: true
205
+ repo_block: true
206
+ # You can schedule full, differential, and incremental backups separately:
207
+ # schedule_full: "0 2 * * 0"
208
+ # schedule_differential: "35 2 * * 1-6"
180
209
  # schedule_incremental: "0 * * * *"
181
210
  ```
182
211
 
@@ -272,11 +301,23 @@ User.all # → Replica
272
301
 
273
302
  ## Requirements
274
303
 
275
- - Ruby 3.0+
276
- - PostgreSQL 12+
304
+ - Ruby 4.0+
305
+ - A PostgreSQL version supported by the target Linux distribution
277
306
  - Ubuntu 20.04+ / Debian 11+ with systemd
278
307
  - SSH key-based authentication (hosts must be in `~/.ssh/known_hosts`)
279
- - Rails 6.0+ (optional)
308
+ - Rails 8.1+ (optional)
309
+
310
+ ## Operational safety
311
+
312
+ - Keep PostgreSQL, PgBouncer, exporters, and backup endpoints behind a private
313
+ network or narrowly scoped firewall rules.
314
+ - Test promotion, application reconnection, backup restore, and point-in-time
315
+ recovery in a disposable environment before using them in production.
316
+ - Review generated configuration and package changes before each rollout;
317
+ automatic failover cannot determine whether an application is safe to write
318
+ after a network partition.
319
+ - Store database, replication, monitoring, and backup credentials in a secret
320
+ manager. Do not commit resolved values or generated configuration.
280
321
 
281
322
  ### SSH host key verification
282
323
 
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...'
@@ -26,6 +26,7 @@ module ActivePostgres
26
26
  ssh_executor.execute_on_host(host) do
27
27
  execute :sudo, 'apt-get', 'remove', '-y', 'pgbackrest'
28
28
  execute :sudo, 'rm', '-f', '/etc/cron.d/pgbackrest-backup'
29
+ execute :sudo, 'rm', '-f', '/etc/cron.d/pgbackrest-backup-differential'
29
30
  execute :sudo, 'rm', '-f', '/etc/cron.d/pgbackrest-backup-incremental'
30
31
  execute :sudo, 'rm', '-f', LOG_ARCHIVE_CRON_FILE
31
32
  execute :sudo, 'rm', '-f', LOG_ARCHIVE_ENV_FILE
@@ -135,9 +136,7 @@ module ActivePostgres
135
136
  execute :sudo, 'chown', "#{postgres_user}:#{postgres_user}", '/var/spool/pgbackrest'
136
137
 
137
138
  # 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
139
+ execute :sudo, '-u', postgres_user, 'pgbackrest', '--stanza=main', 'stanza-create' if create_stanza
141
140
  end
142
141
 
143
142
  if log_archive_enabled?(pgbackrest_config)
@@ -164,15 +163,13 @@ module ActivePostgres
164
163
 
165
164
  def backup_schedules(pgbackrest_config)
166
165
  schedule_full = pgbackrest_config[:schedule_full] || pgbackrest_config[:schedule]
166
+ schedule_differential = pgbackrest_config[:schedule_differential]
167
167
  schedule_incremental = pgbackrest_config[:schedule_incremental]
168
168
 
169
169
  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
170
+ schedules << { type: 'full', schedule: schedule_full, file: '/etc/cron.d/pgbackrest-backup' } if schedule_full
171
+ schedules << { type: 'diff', schedule: schedule_differential, file: '/etc/cron.d/pgbackrest-backup-differential' } if schedule_differential
172
+ schedules << { type: 'incr', schedule: schedule_incremental, file: '/etc/cron.d/pgbackrest-backup-incremental' } if schedule_incremental
176
173
 
177
174
  schedules
178
175
  end
@@ -198,15 +195,14 @@ module ActivePostgres
198
195
  def remove_backup_schedule(host)
199
196
  ssh_executor.execute_on_host(host) do
200
197
  execute :sudo, 'rm', '-f', '/etc/cron.d/pgbackrest-backup'
198
+ execute :sudo, 'rm', '-f', '/etc/cron.d/pgbackrest-backup-differential'
201
199
  execute :sudo, 'rm', '-f', '/etc/cron.d/pgbackrest-backup-incremental'
202
200
  end
203
201
  end
204
202
 
205
203
  def setup_log_archive(host, pgbackrest_config)
206
204
  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
205
+ raise Error, 'pgbackrest.log_archive requires pgbackrest.repo_type: s3' unless pgbackrest_config[:repo_type] == 's3'
210
206
 
211
207
  postgres_user = config.postgres_user
212
208
  env_content = log_archive_env(pgbackrest_config, log_archive_config, host)
@@ -260,13 +256,13 @@ module ActivePostgres
260
256
  'POSTGRES_LOG_ARCHIVE_NODE' => config.node_label_for(host) || host
261
257
  }.compact
262
258
 
263
- env.map { |key, value| "#{key}=#{Shellwords.escape(value.to_s)}" }.join("\n") + "\n"
259
+ "#{env.map { |key, value| "#{key}=#{Shellwords.escape(value.to_s)}" }.join("\n")}\n"
264
260
  end
265
261
 
266
262
  def s3_endpoint_url(pgbackrest_config)
267
263
  endpoint = pgbackrest_config[:s3_endpoint]
268
264
  return nil if endpoint.to_s.empty?
269
- return endpoint if endpoint.match?(/\Ahttps?:\/\//)
265
+ return endpoint if endpoint.match?(%r{\Ahttps?://})
270
266
 
271
267
  "https://#{endpoint}"
272
268
  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"