active_postgres 0.4.0 → 0.5.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: dbeebfa934a6587194e20aa39a3d459b7db2568c996f4faec1b781d70a4254ee
4
- data.tar.gz: 47867c12a1fbb44652da9a79bb775ce38aaf8ff7b527a0a14ce0e042ca091877
3
+ metadata.gz: 0c54d617bfe700f38f1be17b6ff66e630bde27185b0dfdbeae52ef9472a25fd2
4
+ data.tar.gz: 92b80059f95ce83b62de7b047c0827906314d43a737360b32c88de67b4b2b11f
5
5
  SHA512:
6
- metadata.gz: cd6849746528e378cc7e5d3c7707b20d974dc44cc10e61f4600b7eaccbfd7c9793e20b8741ae7339ab866618aa7357ee6781bf4d7ba7ee0eff959ad9c0af1b24
7
- data.tar.gz: fc7635c0908c5920e4f21f702badd030c305819b05e63e6a0a6b54793b563e7338738445d38f11e1269566afba9fb0e6efcbea436287ace088518f6bce42a777
6
+ metadata.gz: 2a55cf101289b0c3a7f88494b79abd6bd61d2f47d9946308da3612dc5be5334013d455c08ef923202679a14f0aba1a1749c33e8164c85f744e5ff32d2fe99776
7
+ data.tar.gz: 1c07e9b4fa3d29a6d11cf960d2836c930d64adca20104b8e2911b2d828e0c19b9e2e20d9b46d4491f22ea9b59b7cc56838c648b1dc1fc40351faece92043bfdd
@@ -54,6 +54,9 @@ module ActivePostgres
54
54
 
55
55
  # Create application users AFTER repmgr to avoid being wiped by cluster recreation
56
56
  create_application_users_if_configured
57
+
58
+ # Update pgbouncer userlist AFTER app users are created so they can authenticate
59
+ update_pgbouncer_userlist if config.component_enabled?(:pgbouncer)
57
60
  end
58
61
 
59
62
  def list_next_steps
@@ -81,5 +84,12 @@ module ActivePostgres
81
84
  core_component = Components::Core.new(config, ssh_executor, secrets)
82
85
  core_component.create_application_users
83
86
  end
87
+
88
+ def update_pgbouncer_userlist
89
+ logger.task('Updating PgBouncer userlist with app users') do
90
+ component = Components::PgBouncer.new(config, ssh_executor, secrets)
91
+ component.update_userlist
92
+ end
93
+ end
84
94
  end
85
95
  end
@@ -4,14 +4,22 @@ module ActivePostgres
4
4
  def install
5
5
  puts 'Installing pgBackRest for backups...'
6
6
 
7
- install_on_host(config.primary_host)
7
+ # Install on primary with full setup (stanza-create)
8
+ install_on_host(config.primary_host, create_stanza: true)
9
+
10
+ # Install on standbys (package + config only, no stanza-create)
11
+ config.standby_hosts.each do |host|
12
+ install_on_host(host, create_stanza: false)
13
+ end
8
14
  end
9
15
 
10
16
  def uninstall
11
17
  puts 'Uninstalling pgBackRest...'
12
18
 
13
- ssh_executor.execute_on_host(config.primary_host) do
14
- execute :sudo, 'apt-get', 'remove', '-y', 'pgbackrest'
19
+ config.all_hosts.each do |host|
20
+ ssh_executor.execute_on_host(host) do
21
+ execute :sudo, 'apt-get', 'remove', '-y', 'pgbackrest'
22
+ end
15
23
  end
16
24
  end
17
25
 
@@ -55,7 +63,7 @@ module ActivePostgres
55
63
 
56
64
  private
57
65
 
58
- def install_on_host(host)
66
+ def install_on_host(host, create_stanza: true)
59
67
  puts " Installing pgBackRest on #{host}..."
60
68
 
61
69
  pgbackrest_config = config.component_config(:pgbackrest)
@@ -86,7 +94,10 @@ module ActivePostgres
86
94
  execute :sudo, 'mkdir', '-p', '/var/spool/pgbackrest'
87
95
  execute :sudo, 'chown', "#{postgres_user}:#{postgres_user}", '/var/spool/pgbackrest'
88
96
 
89
- execute :sudo, '-u', postgres_user, 'pgbackrest', '--stanza=main', 'stanza-create'
97
+ # Only create stanza on primary - standbys share the same backup repo
98
+ if create_stanza
99
+ execute :sudo, '-u', postgres_user, 'pgbackrest', '--stanza=main', 'stanza-create'
100
+ end
90
101
  end
91
102
  end
92
103
  end
@@ -4,27 +4,49 @@ module ActivePostgres
4
4
  def install
5
5
  puts 'Installing PgBouncer for connection pooling...'
6
6
 
7
- # Install on primary (can also install on standbys if needed)
8
- install_on_host(config.primary_host)
7
+ config.all_hosts.each do |host|
8
+ install_on_host(host)
9
+ end
9
10
  end
10
11
 
11
12
  def uninstall
12
13
  puts 'Uninstalling PgBouncer...'
13
14
 
14
- ssh_executor.execute_on_host(config.primary_host) do
15
- execute :sudo, 'systemctl', 'stop', 'pgbouncer'
16
- execute :sudo, 'apt-get', 'remove', '-y', 'pgbouncer'
15
+ config.all_hosts.each do |host|
16
+ ssh_executor.execute_on_host(host) do
17
+ execute :sudo, 'systemctl', 'stop', 'pgbouncer'
18
+ execute :sudo, 'apt-get', 'remove', '-y', 'pgbouncer'
19
+ end
17
20
  end
18
21
  end
19
22
 
20
23
  def restart
21
24
  puts 'Restarting PgBouncer...'
22
25
 
23
- ssh_executor.execute_on_host(config.primary_host) do
24
- execute :sudo, 'systemctl', 'restart', 'pgbouncer'
26
+ config.all_hosts.each do |host|
27
+ ssh_executor.execute_on_host(host) do
28
+ execute :sudo, 'systemctl', 'restart', 'pgbouncer'
29
+ end
25
30
  end
26
31
  end
27
32
 
33
+ def update_userlist
34
+ puts 'Updating PgBouncer userlist on all hosts...'
35
+
36
+ config.all_hosts.each do |host|
37
+ create_userlist(host)
38
+
39
+ ssh_executor.execute_on_host(host) do
40
+ execute :sudo, 'systemctl', 'reload', 'pgbouncer'
41
+ end
42
+ end
43
+ end
44
+
45
+ def install_on_standby(standby_host)
46
+ puts "Installing PgBouncer on standby #{standby_host}..."
47
+ install_on_host(standby_host)
48
+ end
49
+
28
50
  private
29
51
 
30
52
  def install_on_host(host)
@@ -0,0 +1 @@
1
+ <%= ActivePostgres::Rails::DatabaseConfig.render_partial('production', app_name: boring_app_name).strip %>
@@ -0,0 +1,85 @@
1
+ # PostgreSQL High Availability Configuration
2
+ # Generated by active_postgres
3
+ #
4
+ # 💡 Either:
5
+ # 1. Edit this file manually with your database servers
6
+ # 2. Use Terraform to auto-generate this file
7
+ #
8
+ # See: config/postgres.example.yml in gem for full examples
9
+
10
+ shared: &shared
11
+ version: 18
12
+ user: ubuntu
13
+ ssh_key: ~/.ssh/id_rsa
14
+
15
+ components:
16
+ core:
17
+ locale: en_US.UTF-8
18
+ encoding: UTF8
19
+ # Optional: Override default PostgreSQL user and app database config
20
+ # postgres_user: postgres
21
+ # app_user: app
22
+ # app_database: app_production
23
+ postgresql:
24
+ listen_addresses: '*'
25
+ port: 5432
26
+ max_connections: 100
27
+ shared_buffers: 256MB
28
+
29
+ repmgr:
30
+ enabled: false
31
+ # Optional: Override default repmgr user/database
32
+ # user: repmgr
33
+ # database: repmgr
34
+
35
+ pgbouncer:
36
+ enabled: false
37
+ # Optional: Override default pgbouncer user
38
+ # user: pgbouncer
39
+
40
+ pgbackrest:
41
+ enabled: false
42
+
43
+ monitoring:
44
+ enabled: false
45
+
46
+ ssl:
47
+ enabled: false
48
+
49
+ development:
50
+ <<: *shared
51
+ primary:
52
+ host: localhost
53
+ port: 5432
54
+
55
+ production:
56
+ <<: *shared
57
+
58
+ # TODO: Configure your primary database server
59
+ # host: Public IP for SSH deployment (like Kamal)
60
+ # private_ip: Private/VPC IP for database connections (optional, falls back to host)
61
+ primary:
62
+ host: YOUR_PRIMARY_PUBLIC_IP
63
+ private_ip: YOUR_PRIMARY_PRIVATE_IP
64
+ label: us-east-1
65
+
66
+ # TODO: Add standby servers for HA (optional)
67
+ # standby:
68
+ # - host: YOUR_STANDBY_PUBLIC_IP
69
+ # private_ip: YOUR_STANDBY_PRIVATE_IP
70
+ # label: us-west-2
71
+
72
+ # Enable components as needed
73
+ components:
74
+ repmgr:
75
+ enabled: false # Enable for HA with standbys
76
+
77
+ # Secrets: Using Rails credentials (update via: rails credentials:edit)
78
+ # The $(command) syntax allows executing commands to fetch secrets
79
+ secrets:
80
+ superuser_password: $(rails runner "puts Rails.application.credentials.dig(:postgres, :superuser_password)")
81
+ replication_password: $(rails runner "puts Rails.application.credentials.dig(:postgres, :replication_password)")
82
+ repmgr_password: $(rails runner "puts Rails.application.credentials.dig(:postgres, :repmgr_password)")
83
+ pgbouncer_password: $(rails runner "puts Rails.application.credentials.dig(:postgres, :password)")
84
+ app_password: $(rails runner "puts Rails.application.credentials.dig(:postgres, :password)")
85
+
@@ -51,6 +51,27 @@ module ActivePostgres
51
51
  end
52
52
  end
53
53
 
54
+ # Check pgbouncer on all hosts
55
+ if config.component_enabled?(:pgbouncer)
56
+ puts
57
+ puts '==> Checking PgBouncer...'
58
+ config.all_hosts.each do |host|
59
+ print "PgBouncer (#{host})... "
60
+ pgbouncer_ok = check_pgbouncer_running(host)
61
+ userlist_ok = check_pgbouncer_userlist(host)
62
+
63
+ if pgbouncer_ok && userlist_ok
64
+ puts '✓'
65
+ elsif pgbouncer_ok && !userlist_ok
66
+ puts '✗ (missing app user in userlist)'
67
+ all_ok = false
68
+ else
69
+ puts '✗'
70
+ all_ok = false
71
+ end
72
+ end
73
+ end
74
+
54
75
  puts
55
76
  if all_ok
56
77
  puts '✓ All checks passed'
@@ -99,7 +120,8 @@ module ActivePostgres
99
120
  label: primary_config&.dig('label') || '-',
100
121
  status: check_postgres_running(config.primary_host) ? '✓ running' : '✗ down',
101
122
  connections: get_connection_count(config.primary_host),
102
- lag: '-'
123
+ lag: '-',
124
+ pgbouncer: check_pgbouncer_status(config.primary_host)
103
125
  }
104
126
 
105
127
  # Standbys
@@ -114,7 +136,8 @@ module ActivePostgres
114
136
  label: standby_config&.dig('label') || '-',
115
137
  status: running ? '✓ streaming' : '✗ down',
116
138
  connections: running ? get_connection_count(host) : 0,
117
- lag: running ? get_replication_lag(host) : '-'
139
+ lag: running ? get_replication_lag(host) : '-',
140
+ pgbouncer: check_pgbouncer_status(host)
118
141
  }
119
142
  end
120
143
 
@@ -128,7 +151,7 @@ module ActivePostgres
128
151
  end
129
152
 
130
153
  def calculate_column_widths(nodes)
131
- {
154
+ cols = {
132
155
  role: [4, nodes.map { |n| n[:role].length }.max].max,
133
156
  host: [4, nodes.map { |n| n[:host].length }.max].max,
134
157
  private_ip: [10, nodes.map { |n| n[:private_ip].to_s.length }.max].max,
@@ -137,12 +160,25 @@ module ActivePostgres
137
160
  conn: 5,
138
161
  lag: [3, nodes.map { |n| n[:lag].to_s.length }.max].max
139
162
  }
163
+
164
+ if config.component_enabled?(:pgbouncer)
165
+ cols[:pgbouncer] = [9, nodes.map { |n| n[:pgbouncer].to_s.length }.max].max
166
+ end
167
+
168
+ cols
140
169
  end
141
170
 
142
171
  def print_table_header(cols)
143
172
  fmt = "%-#{cols[:role]}s %-#{cols[:host]}s %-#{cols[:private_ip]}s " \
144
173
  "%-#{cols[:label]}s %-#{cols[:status]}s %#{cols[:conn]}s %#{cols[:lag]}s"
145
- header = format(fmt, 'Role', 'Host', 'Private IP', 'Label', 'Status', 'Conn', 'Lag')
174
+ headers = %w[Role Host Private\ IP Label Status Conn Lag]
175
+
176
+ if cols[:pgbouncer]
177
+ fmt += " %-#{cols[:pgbouncer]}s"
178
+ headers << 'PgBouncer'
179
+ end
180
+
181
+ header = format(fmt, *headers)
146
182
  puts header
147
183
  puts '-' * header.length
148
184
  end
@@ -150,8 +186,15 @@ module ActivePostgres
150
186
  def print_table_row(node, cols)
151
187
  fmt = "%-#{cols[:role]}s %-#{cols[:host]}s %-#{cols[:private_ip]}s " \
152
188
  "%-#{cols[:label]}s %-#{cols[:status]}s %#{cols[:conn]}d %#{cols[:lag]}s"
153
- puts format(fmt, node[:role], node[:host], node[:private_ip], node[:label],
154
- node[:status], node[:connections], node[:lag])
189
+ values = [node[:role], node[:host], node[:private_ip], node[:label],
190
+ node[:status], node[:connections], node[:lag]]
191
+
192
+ if cols[:pgbouncer]
193
+ fmt += " %-#{cols[:pgbouncer]}s"
194
+ values << node[:pgbouncer]
195
+ end
196
+
197
+ puts format(fmt, *values)
155
198
  end
156
199
 
157
200
  def print_components
@@ -240,5 +283,37 @@ module ActivePostgres
240
283
  mb = kb / 1024.0
241
284
  "#{mb.round(1)} MB"
242
285
  end
286
+
287
+ def check_pgbouncer_status(host)
288
+ return '-' unless config.component_enabled?(:pgbouncer)
289
+
290
+ ssh_executor.execute_on_host(host) do
291
+ result = capture(:sudo, 'systemctl', 'is-active', 'pgbouncer').strip
292
+ result == 'active' ? '✓ running' : '✗ down'
293
+ end
294
+ rescue StandardError
295
+ '✗ down'
296
+ end
297
+
298
+ def check_pgbouncer_running(host)
299
+ ssh_executor.execute_on_host(host) do
300
+ result = capture(:sudo, 'systemctl', 'is-active', 'pgbouncer').strip
301
+ result == 'active'
302
+ end
303
+ rescue StandardError
304
+ false
305
+ end
306
+
307
+ def check_pgbouncer_userlist(host)
308
+ app_user = config.app_user
309
+ return true unless app_user # No app user configured, skip check
310
+
311
+ ssh_executor.execute_on_host(host) do
312
+ userlist = capture(:sudo, 'cat', '/etc/pgbouncer/userlist.txt').strip
313
+ userlist.include?(app_user)
314
+ end
315
+ rescue StandardError
316
+ false
317
+ end
243
318
  end
244
319
  end
@@ -280,7 +280,9 @@ module ActivePostgres
280
280
  keys_only: true,
281
281
  forward_agent: false,
282
282
  auth_methods: ['publickey'],
283
- verify_host_key: :never
283
+ verify_host_key: :never,
284
+ timeout: 10,
285
+ number_of_password_prompts: 0
284
286
  }
285
287
  end
286
288
  end
@@ -95,7 +95,7 @@ module ActivePostgres
95
95
  def deploy_pgbouncer
96
96
  logger.task('Setting up pgbouncer on standby') do
97
97
  component = Components::PgBouncer.new(config, ssh_executor, secrets)
98
- component.install_on_standby(standby_host) if component.respond_to?(:install_on_standby)
98
+ component.install_on_standby(standby_host)
99
99
  end
100
100
  end
101
101
 
@@ -1,3 +1,3 @@
1
1
  module ActivePostgres
2
- VERSION = '0.4.0'.freeze
2
+ VERSION = '0.5.0'.freeze
3
3
  end
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.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - BoringCache
@@ -172,6 +172,8 @@ files:
172
172
  - lib/active_postgres/error_handler.rb
173
173
  - lib/active_postgres/failover.rb
174
174
  - lib/active_postgres/generators/active_postgres/install_generator.rb
175
+ - lib/active_postgres/generators/active_postgres/templates/database.active_postgres.yml.erb
176
+ - lib/active_postgres/generators/active_postgres/templates/postgres.yml.erb
175
177
  - lib/active_postgres/health_checker.rb
176
178
  - lib/active_postgres/installer.rb
177
179
  - lib/active_postgres/log_sanitizer.rb
@@ -218,7 +220,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
218
220
  - !ruby/object:Gem::Version
219
221
  version: '0'
220
222
  requirements: []
221
- rubygems_version: 3.6.7
223
+ rubygems_version: 3.7.2
222
224
  specification_version: 4
223
225
  summary: PostgreSQL High Availability for Rails, made simple
224
226
  test_files: []