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 +4 -4
- data/lib/active_postgres/components/base.rb +10 -0
- data/lib/active_postgres/components/core.rb +0 -10
- data/lib/active_postgres/components/pgbouncer.rb +21 -8
- data/lib/active_postgres/components/repmgr.rb +6 -0
- data/lib/active_postgres/configuration.rb +10 -1
- data/lib/active_postgres/direct_executor.rb +92 -0
- data/lib/active_postgres/health_checker.rb +24 -17
- data/lib/active_postgres/version.rb +1 -1
- data/lib/active_postgres.rb +1 -0
- data/templates/pgbouncer.ini.erb +8 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0266b3f7dabc2d844b30aa6410cfd7c59131a7fb77a2dea85f01f9a66c2a2443
|
|
4
|
+
data.tar.gz: fd720306229f1ddea1d4e01a9dc3a5df3b7a60a620c38e86779e82f9698ebcbc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|
-
|
|
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, :
|
|
3
|
+
attr_reader :config, :executor
|
|
4
4
|
|
|
5
5
|
def initialize(config)
|
|
6
6
|
@config = config
|
|
7
|
-
@
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
308
|
-
|
|
309
|
-
|
|
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
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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
|
data/lib/active_postgres.rb
CHANGED
|
@@ -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'
|
data/templates/pgbouncer.ini.erb
CHANGED
|
@@ -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.
|
|
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.
|
|
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: []
|