active_postgres 0.5.0 → 0.7.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: 0c54d617bfe700f38f1be17b6ff66e630bde27185b0dfdbeae52ef9472a25fd2
4
- data.tar.gz: 92b80059f95ce83b62de7b047c0827906314d43a737360b32c88de67b4b2b11f
3
+ metadata.gz: 503ec5557f2365458cfe544af059bb5636e82be153a58926978c62ff43384b6f
4
+ data.tar.gz: 20d9bad0c0cbde26d214d304f379854d8d8dd548f06f86462840dad56f0f4580
5
5
  SHA512:
6
- metadata.gz: 2a55cf101289b0c3a7f88494b79abd6bd61d2f47d9946308da3612dc5be5334013d455c08ef923202679a14f0aba1a1749c33e8164c85f744e5ff32d2fe99776
7
- data.tar.gz: 1c07e9b4fa3d29a6d11cf960d2836c930d64adca20104b8e2911b2d828e0c19b9e2e20d9b46d4491f22ea9b59b7cc56838c648b1dc1fc40351faece92043bfdd
6
+ metadata.gz: 26ad8c310458b4e4ecdce1586b98cce9b062260b060e8779481e965c3d2c3d092a93691956372dbf62bd3e138d2b68e97b9dc135748da8a5f4481b683d739c1b
7
+ data.tar.gz: c9ee9f54f725cf43398815a79c621579ac1dad70b36ab997a413905a8cd5222eea42ca0e3397928f26ccf48fc1f1c10da18d9aef31cabfd62eff4ab3aadd0f84
@@ -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: '-')
@@ -11,11 +11,16 @@ module ActivePostgres
11
11
  # See: create_application_user_and_database method called from deployment flow
12
12
 
13
13
  # Install on standbys
14
- # If repmgr is enabled, only install packages (cluster will be cloned by repmgr)
15
- # If repmgr is disabled, install everything including cluster creation
16
14
  config.standby_hosts.each do |host|
17
15
  if config.component_enabled?(:repmgr)
18
- install_packages_only(host)
16
+ # Check if cluster already exists (config update vs fresh install)
17
+ if cluster_exists?(host)
18
+ # Existing cluster - just update configs
19
+ update_configs_on_host(host)
20
+ else
21
+ # Fresh install - only install packages, repmgr will clone the cluster
22
+ install_packages_only(host)
23
+ end
19
24
  else
20
25
  install_on_host(host, is_primary: false)
21
26
  end
@@ -62,6 +67,10 @@ module ActivePostgres
62
67
  else
63
68
  component_config[:postgresql] || {}
64
69
  end
70
+
71
+ # Substitute ${private_ip} with the host's actual private IP
72
+ private_ip = config.replication_host_for(host)
73
+ pg_config = substitute_private_ip(pg_config, private_ip)
65
74
  _ = pg_config # Used in ERB template
66
75
 
67
76
  upload_template(host, 'postgresql.conf.erb', "/etc/postgresql/#{config.version}/main/postgresql.conf", binding,
@@ -92,6 +101,84 @@ module ActivePostgres
92
101
  ssh_executor.install_postgres(host, config.version)
93
102
  end
94
103
 
104
+ def cluster_exists?(host)
105
+ exists = false
106
+ version = config.version
107
+ ssh_executor.execute_on_host(host) do
108
+ exists = test(:sudo, 'test', '-d', "/var/lib/postgresql/#{version}/main/base")
109
+ end
110
+ exists
111
+ end
112
+
113
+ def update_configs_on_host(host)
114
+ puts " Updating configs on #{host}..."
115
+
116
+ component_config = config.component_config(:core)
117
+
118
+ pg_config = if config.component_enabled?(:performance_tuning)
119
+ tuned = calculate_tuned_settings(host, component_config)
120
+ # Standbys must have replication-critical settings >= primary
121
+ ensure_standby_compatible(tuned)
122
+ else
123
+ component_config[:postgresql] || {}
124
+ end
125
+
126
+ private_ip = config.replication_host_for(host)
127
+ pg_config = substitute_private_ip(pg_config, private_ip)
128
+ _ = pg_config
129
+
130
+ upload_template(host, 'postgresql.conf.erb', "/etc/postgresql/#{config.version}/main/postgresql.conf", binding,
131
+ owner: 'postgres:postgres')
132
+ upload_template(host, 'pg_hba.conf.erb', "/etc/postgresql/#{config.version}/main/pg_hba.conf", binding,
133
+ owner: 'postgres:postgres')
134
+
135
+ ssh_executor.restart_postgres(host, config.version)
136
+ end
137
+
138
+ def ensure_standby_compatible(pg_config)
139
+ primary_settings = get_primary_replication_settings
140
+ return pg_config if primary_settings.empty?
141
+
142
+ adjustments = []
143
+
144
+ %i[max_connections max_worker_processes max_wal_senders
145
+ max_prepared_transactions max_locks_per_transaction].each do |setting|
146
+ primary_val = primary_settings[setting]
147
+ next unless primary_val
148
+
149
+ current_val = pg_config[setting]
150
+ if current_val.nil? || current_val.to_i < primary_val.to_i
151
+ adjustments << "#{setting}: #{current_val || 'unset'} → #{primary_val}"
152
+ pg_config[setting] = primary_val
153
+ end
154
+ end
155
+
156
+ if adjustments.any?
157
+ puts " ⚠️ Adjusting standby settings to match primary minimum:"
158
+ adjustments.each { |adj| puts " #{adj}" }
159
+ end
160
+
161
+ pg_config
162
+ end
163
+
164
+ def get_primary_replication_settings
165
+ @primary_replication_settings ||= begin
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')"
168
+ result = ssh_executor.run_sql(config.primary_host, sql)
169
+ result.strip.split("\n").each do |line|
170
+ name, val = line.split('=')
171
+ next unless name && val
172
+
173
+ settings[name.strip.to_sym] = val.strip.to_i
174
+ end
175
+ settings
176
+ rescue StandardError => e
177
+ puts " Warning: Could not get primary settings: #{e.message}"
178
+ {}
179
+ end
180
+ end
181
+
95
182
  def create_app_user_and_database(host)
96
183
  app_user = config.app_user
97
184
  app_database = config.app_database
@@ -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
@@ -14,7 +14,6 @@ module ActivePostgres
14
14
  end
15
15
 
16
16
  def restart
17
- # SSL doesn't have its own service, restart PostgreSQL
18
17
  ssh_executor.restart_postgres(config.primary_host)
19
18
 
20
19
  config.standby_hosts.each do |host|
@@ -37,7 +36,6 @@ module ActivePostgres
37
36
 
38
37
  ssh_executor.ensure_postgres_user(host)
39
38
 
40
- # Ensure the PostgreSQL config directory exists
41
39
  ssh_executor.execute_on_host(host) do
42
40
  execute :sudo, 'mkdir', '-p', "/etc/postgresql/#{version}/main"
43
41
  execute :sudo, 'chown', 'postgres:postgres', "/etc/postgresql/#{version}/main"
@@ -47,17 +45,31 @@ module ActivePostgres
47
45
  ssl_key = secrets.resolve('ssl_key')
48
46
 
49
47
  if ssl_cert && ssl_key
50
- puts ' Using SSL certificates from secrets...'
51
- ssh_executor.upload_file(host, ssl_cert, "/etc/postgresql/#{version}/main/server.crt", mode: '644',
52
- owner: 'postgres:postgres')
53
- ssh_executor.upload_file(host, ssl_key, "/etc/postgresql/#{version}/main/server.key", mode: '600',
54
- owner: 'postgres:postgres')
48
+ install_custom_cert(host, ssl_cert, ssl_key, ssl_config)
55
49
  else
56
50
  puts ' Generating self-signed SSL certificates...'
57
51
  generate_self_signed_cert(host, ssl_config)
58
52
  end
59
53
  end
60
54
 
55
+ def install_custom_cert(host, ssl_cert, ssl_key, _ssl_config)
56
+ version = config.version
57
+ ssl_chain = secrets.resolve('ssl_chain')
58
+
59
+ puts ' Using SSL certificates from secrets...'
60
+
61
+ full_cert = if ssl_chain
62
+ "#{ssl_cert.strip}\n#{ssl_chain.strip}\n"
63
+ else
64
+ ssl_cert
65
+ end
66
+
67
+ ssh_executor.upload_file(host, full_cert, "/etc/postgresql/#{version}/main/server.crt",
68
+ mode: '644', owner: 'postgres:postgres')
69
+ ssh_executor.upload_file(host, ssl_key, "/etc/postgresql/#{version}/main/server.key",
70
+ mode: '600', owner: 'postgres:postgres')
71
+ end
72
+
61
73
  def generate_self_signed_cert(host, ssl_config)
62
74
  version = config.version
63
75
  cert_path = "/etc/postgresql/#{version}/main/server.crt"
@@ -75,11 +75,11 @@ production:
75
75
  enabled: false # Enable for HA with standbys
76
76
 
77
77
  # Secrets: Using Rails credentials (update via: rails credentials:edit)
78
- # The $(command) syntax allows executing commands to fetch secrets
78
+ # The rails_credentials: prefix fetches values from Rails.application.credentials
79
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)")
80
+ superuser_password: rails_credentials:postgres.superuser_password
81
+ replication_password: rails_credentials:postgres.replication_password
82
+ repmgr_password: rails_credentials:postgres.repmgr_password
83
+ pgbouncer_password: rails_credentials:postgres.password
84
+ app_password: rails_credentials:postgres.password
85
85
 
@@ -280,7 +280,7 @@ 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: :accept_new,
284
284
  timeout: 10,
285
285
  number_of_password_prompts: 0
286
286
  }
@@ -1,3 +1,3 @@
1
1
  module ActivePostgres
2
- VERSION = '0.5.0'.freeze
2
+ VERSION = '0.7.0'.freeze
3
3
  end
@@ -336,6 +336,24 @@ namespace :postgres do
336
336
  end
337
337
 
338
338
  namespace :setup do
339
+ desc 'Setup only core PostgreSQL (updates postgresql.conf and pg_hba.conf)'
340
+ task core: :environment do
341
+ require 'active_postgres'
342
+
343
+ config = ActivePostgres::Configuration.load
344
+ installer = ActivePostgres::Installer.new(config)
345
+ installer.setup_component('core')
346
+ end
347
+
348
+ desc 'Setup only SSL certificates'
349
+ task ssl: :environment do
350
+ require 'active_postgres'
351
+
352
+ config = ActivePostgres::Configuration.load
353
+ installer = ActivePostgres::Installer.new(config)
354
+ installer.setup_component('ssl')
355
+ end
356
+
339
357
  desc 'Setup only PgBouncer'
340
358
  task pgbouncer: :environment do
341
359
  require 'active_postgres'
@@ -508,6 +526,18 @@ namespace :postgres do
508
526
  key_valid = test('[ -f /etc/postgresql/*/main/server.key ]')
509
527
  if cert_valid && key_valid
510
528
  info ' Certificates: Present ✅'
529
+ cert_issuer = begin
530
+ capture(:sudo, 'openssl', 'x509', '-in', "/etc/postgresql/#{config.version}/main/server.crt",
531
+ '-noout', '-issuer', '2>/dev/null').strip
532
+ rescue StandardError
533
+ nil
534
+ end
535
+ if cert_issuer
536
+ issuer_o = cert_issuer.match(/O\s*=\s*"?([^",\/]+)"?/)&.captures&.first
537
+ issuer_cn = cert_issuer.match(/CN\s*=\s*([^,\/]+)/)&.captures&.first
538
+ issuer_name = issuer_o || issuer_cn || cert_issuer.sub('issuer=', '')
539
+ info " Issuer: #{issuer_name.strip}"
540
+ end
511
541
  results[:passed] << "#{label}: SSL enabled with certificates"
512
542
  else
513
543
  warn ' Certificates: Missing ⚠️'
@@ -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.5.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - BoringCache