active_postgres 0.6.0 → 0.8.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: cccb8eff8b3ff2ae0f3413bef89b81a1b5b487e474222a9ff3e85fc74e57c5e9
4
- data.tar.gz: 05a21ba9f1a29b6b5d451b3c059fd6a15135b8d6ecd7bad7d6232e6c30a5d63a
3
+ metadata.gz: 0266b3f7dabc2d844b30aa6410cfd7c59131a7fb77a2dea85f01f9a66c2a2443
4
+ data.tar.gz: fd720306229f1ddea1d4e01a9dc3a5df3b7a60a620c38e86779e82f9698ebcbc
5
5
  SHA512:
6
- metadata.gz: '05819a2a73506fb087831e9ad69b6901b8bd6f14f0559a8e08d8c7fbbbff5c20eabc616bf0c862dd95757dab18a52666d8c0c1c00a63dc23b2094849160ce80b'
7
- data.tar.gz: 2733384099851358c38788c41f77768e7a781cd99b7492aa4c10c85758a520167ad954657e0842d7b55681b2f932515933e887706ba3e2d39a50c44fdbd1e7e5
6
+ metadata.gz: bb3bc17966f9ebe98a55f09f678fe2bc2e57b92615c45848c347a7568f6e3bb61d15788d6bb16008e4498a9b944a3f53241f2e2579b204d28679341b041f4978
7
+ data.tar.gz: 3863cace22d1ba3aea901d312d26152de067c7ca898ddff5b654a75702a9e6b4c01f6ffbac820fed338fc5db8ce011ad39676143f81f785f66f7fec72cd72ae5
@@ -23,6 +23,16 @@ module ActivePostgres
23
23
 
24
24
  protected
25
25
 
26
+ def substitute_private_ip(pg_config, private_ip)
27
+ pg_config.transform_values do |value|
28
+ if value.is_a?(String)
29
+ value.gsub('${private_ip}', private_ip)
30
+ else
31
+ value
32
+ end
33
+ end
34
+ end
35
+
26
36
  def render_template(template_name, binding_context)
27
37
  template_path = File.join(ActivePostgres.root, 'templates', template_name)
28
38
  template = ERB.new(File.read(template_path), trim_mode: '-')
@@ -96,16 +96,6 @@ module ActivePostgres
96
96
  optimal_settings.merge(user_postgresql)
97
97
  end
98
98
 
99
- def substitute_private_ip(pg_config, private_ip)
100
- pg_config.transform_values do |value|
101
- if value.is_a?(String)
102
- value.gsub('${private_ip}', private_ip)
103
- else
104
- value
105
- end
106
- end
107
- end
108
-
109
99
  def install_packages_only(host)
110
100
  puts " Installing packages on #{host} (cluster will be created by repmgr)..."
111
101
  ssh_executor.install_postgres(host, config.version)
@@ -52,37 +52,50 @@ module ActivePostgres
52
52
  def install_on_host(host)
53
53
  puts " Installing PgBouncer on #{host}..."
54
54
 
55
- # Get user config
56
55
  user_config = config.component_config(:pgbouncer)
57
56
 
58
- # Calculate optimal pool settings based on PostgreSQL max_connections
59
57
  max_connections = get_postgres_max_connections(host)
60
58
  optimal_pool = ConnectionPooler.calculate_optimal_pool_sizes(max_connections)
61
59
 
62
- # Merge: user config overrides calculated settings
63
60
  pgbouncer_config = optimal_pool.merge(user_config)
64
- _ = pgbouncer_config # Used in ERB template
61
+ ssl_enabled = config.component_enabled?(:ssl)
65
62
 
66
63
  puts " Calculated pool settings for max_connections=#{max_connections}"
67
64
 
68
- # Install package
69
65
  ssh_executor.execute_on_host(host) do
70
66
  execute :sudo, 'apt-get', 'install', '-y', '-qq', 'pgbouncer'
71
67
  end
72
68
 
73
- # Upload configuration
74
69
  upload_template(host, 'pgbouncer.ini.erb', '/etc/pgbouncer/pgbouncer.ini', binding, mode: '644')
75
70
 
76
- # Create userlist with postgres superuser and app user
71
+ setup_ssl_certs(host) if ssl_enabled
72
+
77
73
  create_userlist(host)
78
74
 
79
- # Enable and start
80
75
  ssh_executor.execute_on_host(host) do
81
76
  execute :sudo, 'systemctl', 'enable', 'pgbouncer'
82
77
  execute :sudo, 'systemctl', 'restart', 'pgbouncer'
83
78
  end
84
79
  end
85
80
 
81
+ def setup_ssl_certs(host)
82
+ puts ' Setting up SSL certificates for PgBouncer...'
83
+ version = config.version
84
+
85
+ ssh_executor.execute_on_host(host) do
86
+ execute :sudo, 'cp', "/etc/postgresql/#{version}/main/server.crt", '/etc/pgbouncer/server.crt'
87
+ execute :sudo, 'cp', "/etc/postgresql/#{version}/main/server.key", '/etc/pgbouncer/server.key'
88
+ execute :sudo, 'chmod', '640', '/etc/pgbouncer/server.key'
89
+ execute :sudo, 'chown', 'postgres:postgres', '/etc/pgbouncer/server.key'
90
+ execute :sudo, 'chown', 'postgres:postgres', '/etc/pgbouncer/server.crt'
91
+ end
92
+
93
+ ssl_chain = secrets.resolve('ssl_chain')
94
+ if ssl_chain
95
+ ssh_executor.upload_file(host, ssl_chain, '/etc/pgbouncer/ca.crt', mode: '644', owner: 'postgres:postgres')
96
+ end
97
+ end
98
+
86
99
  def get_postgres_max_connections(host)
87
100
  # Try to get max_connections from running PostgreSQL
88
101
  postgres_user = config.postgres_user
@@ -107,6 +107,9 @@ module ActivePostgres
107
107
 
108
108
  # Performance tuning is handled by the Core component
109
109
  pg_config = component_config[:postgresql] || {}
110
+ # Substitute ${private_ip} with the host's actual private IP
111
+ private_ip = config.replication_host_for(host)
112
+ pg_config = substitute_private_ip(pg_config, private_ip)
110
113
  _ = pg_config # Used in ERB template
111
114
 
112
115
  upload_template(host, 'postgresql.conf.erb', "/etc/postgresql/#{version}/main/postgresql.conf", binding,
@@ -307,6 +310,9 @@ module ActivePostgres
307
310
 
308
311
  # Performance tuning is handled by the Core component
309
312
  pg_config = component_config[:postgresql] || {}
313
+ # Substitute ${private_ip} with the standby's actual private IP
314
+ private_ip = config.replication_host_for(standby_host)
315
+ pg_config = substitute_private_ip(pg_config, private_ip)
310
316
  _ = pg_config # Used in ERB template
311
317
 
312
318
  ssh_executor.execute_on_host(standby_host) do
@@ -8,7 +8,6 @@ module ActivePostgres
8
8
  @environment = environment
9
9
  env_config = config_hash[environment] || {}
10
10
 
11
- # Check if deployment should be skipped (e.g., for development)
12
11
  @skip_deployment = env_config['skip_deployment'] == true
13
12
 
14
13
  @version = env_config['version'] || 18
@@ -65,6 +64,16 @@ module ActivePostgres
65
64
  private_ip_for(node) || host
66
65
  end
67
66
 
67
+ # Returns the host to use for direct PostgreSQL connections (private_ip preferred)
68
+ def connection_host_for(host)
69
+ node = node_config_for(host)
70
+ private_ip_for(node) || host
71
+ end
72
+
73
+ def primary_connection_host
74
+ connection_host_for(primary_host)
75
+ end
76
+
68
77
  def standby_config_for(host)
69
78
  @standbys.find { |s| s['host'] == host }
70
79
  end
@@ -0,0 +1,92 @@
1
+ require 'pg'
2
+
3
+ module ActivePostgres
4
+ class DirectExecutor
5
+ attr_reader :config
6
+
7
+ def initialize(config, quiet: false)
8
+ @config = config
9
+ @quiet = quiet
10
+ end
11
+
12
+ def quiet?
13
+ @quiet
14
+ end
15
+
16
+ def postgres_running?(host)
17
+ with_connection(host) do |conn|
18
+ conn.exec('SELECT 1')
19
+ true
20
+ end
21
+ rescue PG::Error
22
+ false
23
+ end
24
+
25
+ def run_sql(host, sql)
26
+ with_connection(host) do |conn|
27
+ result = conn.exec(sql)
28
+ result.values.flatten.join("\n")
29
+ end
30
+ rescue PG::Error => e
31
+ raise Error, "Failed to execute SQL on #{host}: #{e.message}"
32
+ end
33
+
34
+ def get_postgres_status(host)
35
+ run_sql(host, 'SELECT version();')
36
+ end
37
+
38
+ private
39
+
40
+ def with_connection(host)
41
+ connection_host = config.connection_host_for(host)
42
+ superuser_password = resolve_secret(config.secrets_config['superuser_password'])
43
+
44
+ conn = PG.connect(
45
+ host: connection_host,
46
+ port: 5432,
47
+ dbname: 'postgres',
48
+ user: config.postgres_user,
49
+ password: superuser_password,
50
+ connect_timeout: 10,
51
+ sslmode: config.component_enabled?(:ssl) ? 'require' : 'prefer'
52
+ )
53
+
54
+ begin
55
+ yield conn
56
+ ensure
57
+ conn.close
58
+ end
59
+ end
60
+
61
+ def resolve_secret(value)
62
+ return nil if value.nil?
63
+
64
+ # Handle Rails credentials
65
+ if value.start_with?('rails_credentials:')
66
+ return resolve_rails_credentials(value)
67
+ end
68
+
69
+ # Handle environment variables
70
+ if value.start_with?('$')
71
+ return ENV[value[1..]]
72
+ end
73
+
74
+ # Handle shell commands
75
+ if value.start_with?('$(') && value.end_with?(')')
76
+ return `#{value[2..-2]}`.strip
77
+ end
78
+
79
+ value
80
+ end
81
+
82
+ def resolve_rails_credentials(value)
83
+ path = value.sub('rails_credentials:', '')
84
+ keys = path.split('.').map(&:to_sym)
85
+
86
+ credentials = Rails.application.credentials
87
+ keys.reduce(credentials) { |obj, key| obj&.dig(key) }
88
+ rescue NameError
89
+ nil
90
+ end
91
+ end
92
+ end
@@ -1,10 +1,19 @@
1
1
  module ActivePostgres
2
2
  class HealthChecker
3
- attr_reader :config, :ssh_executor
3
+ attr_reader :config, :executor
4
4
 
5
5
  def initialize(config)
6
6
  @config = config
7
- @ssh_executor = SSHExecutor.new(config, quiet: true)
7
+ @executor = create_executor
8
+ end
9
+
10
+ # Backwards compatibility alias
11
+ def ssh_executor
12
+ @executor
13
+ end
14
+
15
+ private def create_executor
16
+ DirectExecutor.new(config, quiet: true)
8
17
  end
9
18
 
10
19
  def show_status
@@ -287,31 +296,29 @@ module ActivePostgres
287
296
  def check_pgbouncer_status(host)
288
297
  return '-' unless config.component_enabled?(:pgbouncer)
289
298
 
290
- ssh_executor.execute_on_host(host) do
291
- result = capture(:sudo, 'systemctl', 'is-active', 'pgbouncer').strip
292
- result == 'active' ? '✓ running' : '✗ down'
293
- end
299
+ check_pgbouncer_direct(host) ? '✓ running' : '✗ down'
294
300
  rescue StandardError
295
301
  '✗ down'
296
302
  end
297
303
 
298
304
  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
305
+ check_pgbouncer_direct(host)
303
306
  rescue StandardError
304
307
  false
305
308
  end
306
309
 
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
+ def check_pgbouncer_userlist(_host)
311
+ true
312
+ end
313
+
314
+ def check_pgbouncer_direct(host)
315
+ require 'socket'
316
+ connection_host = config.connection_host_for(host)
317
+ pgbouncer_port = config.component_config(:pgbouncer)[:listen_port] || 6432
310
318
 
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
319
+ socket = TCPSocket.new(connection_host, pgbouncer_port)
320
+ socket.close
321
+ true
315
322
  rescue StandardError
316
323
  false
317
324
  end
@@ -1,3 +1,3 @@
1
1
  module ActivePostgres
2
- VERSION = '0.6.0'.freeze
2
+ VERSION = '0.8.0'.freeze
3
3
  end
@@ -21,6 +21,7 @@ require_relative 'active_postgres/standby_deployment_flow'
21
21
  require_relative 'active_postgres/cli'
22
22
  require_relative 'active_postgres/installer'
23
23
  require_relative 'active_postgres/ssh_executor'
24
+ require_relative 'active_postgres/direct_executor'
24
25
  require_relative 'active_postgres/health_checker'
25
26
  require_relative 'active_postgres/failover'
26
27
  require_relative 'active_postgres/performance_tuner'
@@ -44,6 +44,14 @@ log_connections = <%= pgbouncer_config[:log_connections] || 1 %>
44
44
  log_disconnections = <%= pgbouncer_config[:log_disconnections] || 1 %>
45
45
  log_pooler_errors = <%= pgbouncer_config[:log_pooler_errors] || 1 %>
46
46
 
47
+ <% if ssl_enabled %>
48
+ # Client TLS (incoming connections from Rails)
49
+ client_tls_sslmode = require
50
+ client_tls_key_file = /etc/pgbouncer/server.key
51
+ client_tls_cert_file = /etc/pgbouncer/server.crt
52
+ client_tls_ca_file = /etc/pgbouncer/ca.crt
53
+ <% end %>
54
+
47
55
  # Process management
48
56
  <% if pgbouncer_config[:pidfile] %>
49
57
  pidfile = <%= pgbouncer_config[:pidfile] %>
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.6.0
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - BoringCache
@@ -169,6 +169,7 @@ files:
169
169
  - lib/active_postgres/connection_pooler.rb
170
170
  - lib/active_postgres/credentials.rb
171
171
  - lib/active_postgres/deployment_flow.rb
172
+ - lib/active_postgres/direct_executor.rb
172
173
  - lib/active_postgres/error_handler.rb
173
174
  - lib/active_postgres/failover.rb
174
175
  - lib/active_postgres/generators/active_postgres/install_generator.rb
@@ -220,7 +221,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
220
221
  - !ruby/object:Gem::Version
221
222
  version: '0'
222
223
  requirements: []
223
- rubygems_version: 3.7.2
224
+ rubygems_version: 3.6.9
224
225
  specification_version: 4
225
226
  summary: PostgreSQL High Availability for Rails, made simple
226
227
  test_files: []