active_postgres 0.5.0 → 0.6.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 +4 -4
- data/lib/active_postgres/components/core.rb +100 -3
- data/lib/active_postgres/components/ssl.rb +19 -7
- data/lib/active_postgres/generators/active_postgres/templates/postgres.yml.erb +6 -6
- data/lib/active_postgres/ssh_executor.rb +1 -1
- data/lib/active_postgres/version.rb +1 -1
- data/lib/tasks/postgres.rake +30 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cccb8eff8b3ff2ae0f3413bef89b81a1b5b487e474222a9ff3e85fc74e57c5e9
|
|
4
|
+
data.tar.gz: 05a21ba9f1a29b6b5d451b3c059fd6a15135b8d6ecd7bad7d6232e6c30a5d63a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '05819a2a73506fb087831e9ad69b6901b8bd6f14f0559a8e08d8c7fbbbff5c20eabc616bf0c862dd95757dab18a52666d8c0c1c00a63dc23b2094849160ce80b'
|
|
7
|
+
data.tar.gz: 2733384099851358c38788c41f77768e7a781cd99b7492aa4c10c85758a520167ad954657e0842d7b55681b2f932515933e887706ba3e2d39a50c44fdbd1e7e5
|
|
@@ -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
|
-
|
|
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,
|
|
@@ -87,11 +96,99 @@ module ActivePostgres
|
|
|
87
96
|
optimal_settings.merge(user_postgresql)
|
|
88
97
|
end
|
|
89
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
|
+
|
|
90
109
|
def install_packages_only(host)
|
|
91
110
|
puts " Installing packages on #{host} (cluster will be created by repmgr)..."
|
|
92
111
|
ssh_executor.install_postgres(host, config.version)
|
|
93
112
|
end
|
|
94
113
|
|
|
114
|
+
def cluster_exists?(host)
|
|
115
|
+
exists = false
|
|
116
|
+
version = config.version
|
|
117
|
+
ssh_executor.execute_on_host(host) do
|
|
118
|
+
exists = test(:sudo, 'test', '-d', "/var/lib/postgresql/#{version}/main/base")
|
|
119
|
+
end
|
|
120
|
+
exists
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def update_configs_on_host(host)
|
|
124
|
+
puts " Updating configs on #{host}..."
|
|
125
|
+
|
|
126
|
+
component_config = config.component_config(:core)
|
|
127
|
+
|
|
128
|
+
pg_config = if config.component_enabled?(:performance_tuning)
|
|
129
|
+
tuned = calculate_tuned_settings(host, component_config)
|
|
130
|
+
# Standbys must have replication-critical settings >= primary
|
|
131
|
+
ensure_standby_compatible(tuned)
|
|
132
|
+
else
|
|
133
|
+
component_config[:postgresql] || {}
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
private_ip = config.replication_host_for(host)
|
|
137
|
+
pg_config = substitute_private_ip(pg_config, private_ip)
|
|
138
|
+
_ = pg_config
|
|
139
|
+
|
|
140
|
+
upload_template(host, 'postgresql.conf.erb', "/etc/postgresql/#{config.version}/main/postgresql.conf", binding,
|
|
141
|
+
owner: 'postgres:postgres')
|
|
142
|
+
upload_template(host, 'pg_hba.conf.erb', "/etc/postgresql/#{config.version}/main/pg_hba.conf", binding,
|
|
143
|
+
owner: 'postgres:postgres')
|
|
144
|
+
|
|
145
|
+
ssh_executor.restart_postgres(host, config.version)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def ensure_standby_compatible(pg_config)
|
|
149
|
+
primary_settings = get_primary_replication_settings
|
|
150
|
+
return pg_config if primary_settings.empty?
|
|
151
|
+
|
|
152
|
+
adjustments = []
|
|
153
|
+
|
|
154
|
+
%i[max_connections max_worker_processes max_wal_senders
|
|
155
|
+
max_prepared_transactions max_locks_per_transaction].each do |setting|
|
|
156
|
+
primary_val = primary_settings[setting]
|
|
157
|
+
next unless primary_val
|
|
158
|
+
|
|
159
|
+
current_val = pg_config[setting]
|
|
160
|
+
if current_val.nil? || current_val.to_i < primary_val.to_i
|
|
161
|
+
adjustments << "#{setting}: #{current_val || 'unset'} → #{primary_val}"
|
|
162
|
+
pg_config[setting] = primary_val
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
if adjustments.any?
|
|
167
|
+
puts " ⚠️ Adjusting standby settings to match primary minimum:"
|
|
168
|
+
adjustments.each { |adj| puts " #{adj}" }
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
pg_config
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def get_primary_replication_settings
|
|
175
|
+
@primary_replication_settings ||= begin
|
|
176
|
+
settings = {}
|
|
177
|
+
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')"
|
|
178
|
+
result = ssh_executor.run_sql(config.primary_host, sql)
|
|
179
|
+
result.strip.split("\n").each do |line|
|
|
180
|
+
name, val = line.split('=')
|
|
181
|
+
next unless name && val
|
|
182
|
+
|
|
183
|
+
settings[name.strip.to_sym] = val.strip.to_i
|
|
184
|
+
end
|
|
185
|
+
settings
|
|
186
|
+
rescue StandardError => e
|
|
187
|
+
puts " Warning: Could not get primary settings: #{e.message}"
|
|
188
|
+
{}
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
95
192
|
def create_app_user_and_database(host)
|
|
96
193
|
app_user = config.app_user
|
|
97
194
|
app_database = config.app_database
|
|
@@ -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
|
-
|
|
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
|
|
78
|
+
# The rails_credentials: prefix fetches values from Rails.application.credentials
|
|
79
79
|
secrets:
|
|
80
|
-
superuser_password:
|
|
81
|
-
replication_password:
|
|
82
|
-
repmgr_password:
|
|
83
|
-
pgbouncer_password:
|
|
84
|
-
app_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
|
|
data/lib/tasks/postgres.rake
CHANGED
|
@@ -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 ⚠️'
|