active_postgres 0.4.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.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +23 -0
  3. data/README.md +158 -0
  4. data/exe/activepostgres +5 -0
  5. data/lib/active_postgres/cli.rb +157 -0
  6. data/lib/active_postgres/cluster_deployment_flow.rb +85 -0
  7. data/lib/active_postgres/component_resolver.rb +24 -0
  8. data/lib/active_postgres/components/base.rb +38 -0
  9. data/lib/active_postgres/components/core.rb +158 -0
  10. data/lib/active_postgres/components/extensions.rb +99 -0
  11. data/lib/active_postgres/components/monitoring.rb +55 -0
  12. data/lib/active_postgres/components/pgbackrest.rb +94 -0
  13. data/lib/active_postgres/components/pgbouncer.rb +137 -0
  14. data/lib/active_postgres/components/repmgr.rb +651 -0
  15. data/lib/active_postgres/components/ssl.rb +86 -0
  16. data/lib/active_postgres/configuration.rb +190 -0
  17. data/lib/active_postgres/connection_pooler.rb +429 -0
  18. data/lib/active_postgres/credentials.rb +17 -0
  19. data/lib/active_postgres/deployment_flow.rb +154 -0
  20. data/lib/active_postgres/error_handler.rb +185 -0
  21. data/lib/active_postgres/failover.rb +83 -0
  22. data/lib/active_postgres/generators/active_postgres/install_generator.rb +186 -0
  23. data/lib/active_postgres/health_checker.rb +244 -0
  24. data/lib/active_postgres/installer.rb +114 -0
  25. data/lib/active_postgres/log_sanitizer.rb +67 -0
  26. data/lib/active_postgres/logger.rb +125 -0
  27. data/lib/active_postgres/performance_tuner.rb +246 -0
  28. data/lib/active_postgres/rails/database_config.rb +174 -0
  29. data/lib/active_postgres/rails/migration_guard.rb +25 -0
  30. data/lib/active_postgres/railtie.rb +28 -0
  31. data/lib/active_postgres/retry_helper.rb +80 -0
  32. data/lib/active_postgres/rollback_manager.rb +140 -0
  33. data/lib/active_postgres/secrets.rb +86 -0
  34. data/lib/active_postgres/ssh_executor.rb +288 -0
  35. data/lib/active_postgres/standby_deployment_flow.rb +122 -0
  36. data/lib/active_postgres/validator.rb +143 -0
  37. data/lib/active_postgres/version.rb +3 -0
  38. data/lib/active_postgres.rb +67 -0
  39. data/lib/tasks/postgres.rake +855 -0
  40. data/lib/tasks/rolling_update.rake +258 -0
  41. data/lib/tasks/rotate_credentials.rake +193 -0
  42. data/templates/pg_hba.conf.erb +47 -0
  43. data/templates/pgbackrest.conf.erb +43 -0
  44. data/templates/pgbouncer.ini.erb +55 -0
  45. data/templates/postgresql.conf.erb +157 -0
  46. data/templates/repmgr.conf.erb +40 -0
  47. metadata +224 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: dbeebfa934a6587194e20aa39a3d459b7db2568c996f4faec1b781d70a4254ee
4
+ data.tar.gz: 47867c12a1fbb44652da9a79bb775ce38aaf8ff7b527a0a14ce0e042ca091877
5
+ SHA512:
6
+ metadata.gz: cd6849746528e378cc7e5d3c7707b20d974dc44cc10e61f4600b7eaccbfd7c9793e20b8741ae7339ab866618aa7357ee6781bf4d7ba7ee0eff959ad9c0af1b24
7
+ data.tar.gz: fc7635c0908c5920e4f21f702badd030c305819b05e63e6a0a6b54793b563e7338738445d38f11e1269566afba9fb0e6efcbea436287ace088518f6bce42a777
data/LICENSE ADDED
@@ -0,0 +1,23 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Gaurav
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+
data/README.md ADDED
@@ -0,0 +1,158 @@
1
+ # active_postgres
2
+
3
+ Production-grade PostgreSQL HA for Rails.
4
+
5
+ ## Features
6
+
7
+ - **High Availability**: Primary/standby replication with automatic failover (repmgr)
8
+ - **Connection Pooling**: PgBouncer integration
9
+ - **Rails Integration**: Automatic database.yml config, migration guard, read replica routing
10
+ - **Modular Components**: Core, Performance Tuning, repmgr, PgBouncer, pgBackRest, Monitoring, SSL, Extensions
11
+
12
+ ## Quick Start
13
+
14
+ ### 1. Install
15
+
16
+ ```bash
17
+ gem install active_postgres
18
+ # or add to Gemfile: gem 'active_postgres'
19
+ ```
20
+
21
+ ### 2. Configure (Rails)
22
+
23
+ ```bash
24
+ rails generate active_postgres:install
25
+ ```
26
+
27
+ Edit `config/postgres.yml`:
28
+
29
+ ```yaml
30
+ production:
31
+ version: 18
32
+ user: ubuntu
33
+ ssh_key: ~/.ssh/id_rsa
34
+
35
+ primary:
36
+ host: 34.12.234.81 # Public IP for SSH
37
+ private_ip: 10.8.0.10 # Private IP for database connections
38
+
39
+ standby:
40
+ - host: 52.23.45.67
41
+ private_ip: 10.8.0.11
42
+
43
+ components:
44
+ repmgr: {enabled: true}
45
+ pgbouncer: {enabled: true}
46
+
47
+ secrets:
48
+ superuser_password: $POSTGRES_SUPERUSER_PASSWORD
49
+ replication_password: $POSTGRES_REPLICATION_PASSWORD
50
+ repmgr_password: $POSTGRES_REPMGR_PASSWORD
51
+ app_password: $POSTGRES_APP_PASSWORD
52
+ ```
53
+
54
+ Add credentials (`rails credentials:edit`):
55
+
56
+ ```yaml
57
+ postgres:
58
+ username: myapp
59
+ password: "your_app_password"
60
+ database: myapp_production
61
+ primary_host: 10.8.0.10
62
+ replica_host: 10.8.0.11
63
+ port: 6432 # 6432 for PgBouncer, 5432 for direct
64
+ ```
65
+
66
+ ### 3. Deploy
67
+
68
+ ```bash
69
+ rake postgres:setup # Deploy cluster
70
+ rake postgres:status # Check health
71
+ ```
72
+
73
+ ## Common Operations
74
+
75
+ ### Rake Tasks
76
+
77
+ ```bash
78
+ rake postgres:setup # Deploy HA cluster
79
+ rake postgres:status # Check cluster status
80
+ rake postgres:verify # Comprehensive health check
81
+ rake postgres:promote[host] # Promote standby to primary
82
+
83
+ # Backups (requires pgBackRest)
84
+ rake postgres:backup:full
85
+ rake postgres:backup:list
86
+ rake postgres:backup:restore[backup_id]
87
+
88
+ # Credential rotation (zero downtime)
89
+ rake postgres:credentials:rotate_random
90
+ rake postgres:credentials:rotate_all
91
+
92
+ # Rolling updates (zero downtime)
93
+ rake postgres:update:version[18] # Major version upgrade
94
+ rake postgres:update:patch # Security patches
95
+ ```
96
+
97
+ ### CLI (Standalone)
98
+
99
+ ```bash
100
+ active_postgres setup --environment=production
101
+ active_postgres setup-standby HOST
102
+ active_postgres status
103
+ active_postgres promote HOST
104
+ active_postgres backup --type=full
105
+ active_postgres cache-secrets
106
+ ```
107
+
108
+ ## Components
109
+
110
+ | Component | Description | Config |
111
+ |-----------|-------------|--------|
112
+ | **Core** | PostgreSQL installation | Always enabled |
113
+ | **Performance Tuning** | Auto-optimization | Enabled by default |
114
+ | **repmgr** | HA & automatic failover | `repmgr: {enabled: true}` |
115
+ | **PgBouncer** | Connection pooling | `pgbouncer: {enabled: true}` |
116
+ | **pgBackRest** | Backup & restore | `pgbackrest: {enabled: true}` |
117
+ | **Monitoring** | postgres_exporter | `monitoring: {enabled: true}` |
118
+ | **SSL** | Encrypted connections | `ssl: {enabled: true}` |
119
+ | **Extensions** | pgvector, PostGIS, etc. | `extensions: {enabled: true, list: [pgvector]}` |
120
+
121
+ ## Secrets Management
122
+
123
+ ```yaml
124
+ secrets:
125
+ # Environment variables
126
+ password: $POSTGRES_PASSWORD
127
+
128
+ # Command execution
129
+ password: $(op read "op://vault/item/field")
130
+ password: $(rails runner "puts Rails.application.credentials.dig(:postgres, :password)")
131
+
132
+ # AWS Secrets Manager
133
+ password: $(aws secretsmanager get-secret-value --secret-id myapp/postgres --query SecretString)
134
+ ```
135
+
136
+ ## Read/Write Splitting (Rails 6+)
137
+
138
+ ```ruby
139
+ class ApplicationRecord < ActiveRecord::Base
140
+ connects_to database: { writing: :primary, reading: :primary_replica }
141
+ end
142
+
143
+ # Writes go to primary, reads go to replica
144
+ User.create(name: "Alice") # → Primary
145
+ User.all # → Replica
146
+ ```
147
+
148
+ ## Requirements
149
+
150
+ - Ruby 3.0+
151
+ - PostgreSQL 12+
152
+ - Ubuntu 20.04+ / Debian 11+ with systemd
153
+ - SSH key-based authentication
154
+ - Rails 6.0+ (optional)
155
+
156
+ ## License
157
+
158
+ MIT
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative '../lib/active_postgres'
4
+
5
+ ActivePostgres::CLI.start(ARGV)
@@ -0,0 +1,157 @@
1
+ require 'thor'
2
+
3
+ module ActivePostgres
4
+ class CLI < Thor
5
+ class_option :environment, aliases: '-e', default: ENV['BORING_ENVIRONMENT'] || ENV['RAILS_ENV'] || 'development'
6
+ class_option :config, aliases: '-c', default: 'config/postgres.yml'
7
+
8
+ desc 'setup', 'Setup PostgreSQL HA cluster'
9
+ option :dry_run, type: :boolean, default: false
10
+ option :only, type: :string, desc: 'Setup only specific component (core, repmgr, pgbouncer, etc.)'
11
+ def setup
12
+ config = load_config
13
+ installer = Installer.new(config, dry_run: options[:dry_run])
14
+
15
+ if options[:only]
16
+ installer.setup_component(options[:only])
17
+ else
18
+ installer.setup
19
+ end
20
+ end
21
+
22
+ desc 'setup-primary', '[DEPRECATED] Use "setup" instead - it auto-detects primary-only vs HA'
23
+ option :dry_run, type: :boolean, default: false
24
+ def setup_primary
25
+ puts 'āš ļø DEPRECATED: Use "active_postgres setup" instead.'
26
+ puts ' The setup command now auto-detects whether to deploy primary-only or HA based on your config.'
27
+ puts ''
28
+ setup
29
+ end
30
+
31
+ desc 'setup-standby HOST', 'Setup a single standby server without touching the primary'
32
+ option :dry_run, type: :boolean, default: false
33
+ def setup_standby(host)
34
+ config = load_config
35
+ installer = Installer.new(config, dry_run: options[:dry_run])
36
+
37
+ unless config.standby_hosts.include?(host)
38
+ puts "Error: #{host} is not configured as a standby in config/postgres.yml"
39
+ exit 1
40
+ end
41
+
42
+ installer.setup_standby_only(host)
43
+ end
44
+
45
+ desc 'status', 'Show cluster status'
46
+ def status
47
+ config = load_config
48
+ health_checker = HealthChecker.new(config)
49
+ health_checker.show_status
50
+ end
51
+
52
+ desc 'health', 'Run health checks'
53
+ def health
54
+ config = load_config
55
+ health_checker = HealthChecker.new(config)
56
+ health_checker.run_health_checks
57
+ end
58
+
59
+ desc 'promote HOST', 'Promote standby to primary'
60
+ option :node, type: :string
61
+ def promote(host = nil)
62
+ host ||= options[:node]
63
+
64
+ unless host
65
+ puts 'Error: Must specify host or --node'
66
+ exit 1
67
+ end
68
+
69
+ config = load_config
70
+ failover = Failover.new(config)
71
+ failover.promote(host)
72
+ end
73
+
74
+ desc 'backup', 'Create backup'
75
+ option :type, default: 'full', desc: 'Backup type: full, incremental'
76
+ def backup
77
+ config = load_config
78
+
79
+ unless config.component_enabled?(:pgbackrest)
80
+ puts 'Error: pgBackRest component not enabled'
81
+ exit 1
82
+ end
83
+
84
+ installer = Installer.new(config)
85
+ installer.run_backup(options[:type])
86
+ end
87
+
88
+ desc 'restore BACKUP_ID', 'Restore from backup'
89
+ def restore(backup_id)
90
+ config = load_config
91
+
92
+ unless config.component_enabled?(:pgbackrest)
93
+ puts 'Error: pgBackRest component not enabled'
94
+ exit 1
95
+ end
96
+
97
+ installer = Installer.new(config)
98
+ installer.run_restore(backup_id)
99
+ end
100
+
101
+ desc 'list-backups', 'List available backups'
102
+ def list_backups
103
+ config = load_config
104
+
105
+ unless config.component_enabled?(:pgbackrest)
106
+ puts 'Error: pgBackRest component not enabled'
107
+ exit 1
108
+ end
109
+
110
+ installer = Installer.new(config)
111
+ installer.list_backups
112
+ end
113
+
114
+ desc 'install COMPONENT', 'Install specific component'
115
+ def install(component)
116
+ config = load_config
117
+ installer = Installer.new(config)
118
+ installer.setup_component(component)
119
+ end
120
+
121
+ desc 'uninstall COMPONENT', 'Uninstall specific component'
122
+ def uninstall(component)
123
+ config = load_config
124
+ installer = Installer.new(config)
125
+ installer.uninstall_component(component)
126
+ end
127
+
128
+ desc 'restart COMPONENT', 'Restart specific component'
129
+ def restart(component)
130
+ config = load_config
131
+ installer = Installer.new(config)
132
+ installer.restart_component(component)
133
+ end
134
+
135
+ desc 'cache-secrets', 'Fetch and cache secrets locally'
136
+ option :directory, default: '.secrets'
137
+ def cache_secrets
138
+ config = load_config
139
+ secrets = Secrets.new(config)
140
+ secrets.cache_to_files(options[:directory])
141
+ end
142
+
143
+ desc 'version', 'Show version'
144
+ def version
145
+ puts "active_postgres #{ActivePostgres::VERSION}"
146
+ end
147
+
148
+ private
149
+
150
+ def load_config
151
+ Configuration.load(options[:config], options[:environment])
152
+ rescue StandardError => e
153
+ puts "Error loading config: #{e.message}"
154
+ exit 1
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,85 @@
1
+ module ActivePostgres
2
+ class ClusterDeploymentFlow < DeploymentFlow
3
+ private
4
+
5
+ def operation_name
6
+ if standbys?
7
+ 'PostgreSQL HA Cluster Setup'
8
+ else
9
+ 'PostgreSQL Primary Setup'
10
+ end
11
+ end
12
+
13
+ def print_targets
14
+ logger.info "Primary: #{config.primary_host}"
15
+ if standbys?
16
+ logger.info "Standbys: #{config.standby_hosts.join(', ')}"
17
+ else
18
+ logger.info 'Standbys: None (primary-only setup)'
19
+ end
20
+ end
21
+
22
+ def validate_specific_requirements
23
+ return unless config.component_enabled?(:repmgr) && !standbys?
24
+
25
+ logger.warn 'āš ļø repmgr is enabled but no standbys configured - will skip repmgr setup'
26
+ end
27
+
28
+ def list_deployment_steps
29
+ if standbys?
30
+ logger.info " • Install/recreate PostgreSQL #{config.version} on all servers"
31
+ logger.info ' • Configure repmgr for high availability' if should_setup_repmgr?
32
+ else
33
+ logger.info " • Install/recreate PostgreSQL #{config.version} on primary"
34
+ end
35
+
36
+ logger.info ' • Setup pgbouncer connection pooling' if config.component_enabled?(:pgbouncer)
37
+ logger.info ' • Configure pgbackrest backups' if config.component_enabled?(:pgbackrest)
38
+ logger.info ' • Install postgres_exporter monitoring' if config.component_enabled?(:monitoring)
39
+ logger.info ' • Enable SSL/TLS connections' if config.component_enabled?(:ssl)
40
+ end
41
+
42
+ def deploy_components
43
+ hosts_to_deploy = standbys? ? config.all_hosts : [config.primary_host]
44
+
45
+ setup_component('ssl', hosts_to_deploy) if config.component_enabled?(:ssl)
46
+ setup_component('core', hosts_to_deploy)
47
+
48
+ components = %i[pgbouncer pgbackrest monitoring extensions]
49
+ components.unshift(:repmgr) if should_setup_repmgr?
50
+
51
+ components.each do |component|
52
+ setup_component(component.to_s, hosts_to_deploy) if config.component_enabled?(component)
53
+ end
54
+
55
+ # Create application users AFTER repmgr to avoid being wiped by cluster recreation
56
+ create_application_users_if_configured
57
+ end
58
+
59
+ def list_next_steps
60
+ logger.info ''
61
+ logger.info 'šŸ“‹ Next Steps:'
62
+ logger.info ' 1. Verify cluster: rake postgres:verify'
63
+ logger.info " 2. Update database.yml to use: #{config.primary_host}:#{config.component_enabled?(:pgbouncer) ? '6432' : '5432'}"
64
+ logger.info ' 3. Run migrations: rake postgres:migrate'
65
+ logger.info ' 4. Update PgBouncer userlist: rake postgres:pgbouncer:update_userlist[your_app_user]' if config.component_enabled?(:pgbouncer)
66
+
67
+ return if standbys?
68
+
69
+ logger.info ' 5. To add HA later: Add standbys to config → run: rake postgres:setup'
70
+ end
71
+
72
+ def standbys?
73
+ config.standby_hosts.any?
74
+ end
75
+
76
+ def should_setup_repmgr?
77
+ config.component_enabled?(:repmgr) && standbys?
78
+ end
79
+
80
+ def create_application_users_if_configured
81
+ core_component = Components::Core.new(config, ssh_executor, secrets)
82
+ core_component.create_application_users
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,24 @@
1
+ module ActivePostgres
2
+ module ComponentResolver
3
+ def component_class_for(component_name)
4
+ case component_name.to_s.downcase
5
+ when 'core'
6
+ Components::Core
7
+ when 'repmgr'
8
+ Components::Repmgr
9
+ when 'pgbouncer'
10
+ Components::PgBouncer
11
+ when 'pgbackrest'
12
+ Components::PgBackRest
13
+ when 'monitoring'
14
+ Components::Monitoring
15
+ when 'ssl'
16
+ Components::SSL
17
+ when 'extensions'
18
+ Components::Extensions
19
+ else
20
+ raise Error, "Unknown component: #{component_name}"
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,38 @@
1
+ module ActivePostgres
2
+ module Components
3
+ class Base
4
+ attr_reader :config, :ssh_executor, :secrets
5
+
6
+ def initialize(config, ssh_executor, secrets)
7
+ @config = config
8
+ @ssh_executor = ssh_executor
9
+ @secrets = secrets
10
+ end
11
+
12
+ def install
13
+ raise NotImplementedError, 'Subclass must implement #install'
14
+ end
15
+
16
+ def uninstall
17
+ raise NotImplementedError, 'Subclass must implement #uninstall'
18
+ end
19
+
20
+ def restart
21
+ raise NotImplementedError, 'Subclass must implement #restart'
22
+ end
23
+
24
+ protected
25
+
26
+ def render_template(template_name, binding_context)
27
+ template_path = File.join(ActivePostgres.root, 'templates', template_name)
28
+ template = ERB.new(File.read(template_path), trim_mode: '-')
29
+ template.result(binding_context)
30
+ end
31
+
32
+ def upload_template(host, template_name, remote_path, binding_context, mode: '644', owner: nil)
33
+ content = render_template(template_name, binding_context)
34
+ ssh_executor.upload_file(host, content, remote_path, mode: mode, owner: owner)
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,158 @@
1
+ module ActivePostgres
2
+ module Components
3
+ class Core < Base
4
+ def install
5
+ puts 'Installing PostgreSQL core...'
6
+
7
+ # Install on primary
8
+ install_on_host(config.primary_host, is_primary: true)
9
+
10
+ # NOTE: App user creation moved to after repmgr setup to avoid being wiped
11
+ # See: create_application_user_and_database method called from deployment flow
12
+
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
+ config.standby_hosts.each do |host|
17
+ if config.component_enabled?(:repmgr)
18
+ install_packages_only(host)
19
+ else
20
+ install_on_host(host, is_primary: false)
21
+ end
22
+ end
23
+ end
24
+
25
+ def uninstall
26
+ puts 'Uninstalling PostgreSQL is not recommended and must be done manually.'
27
+ end
28
+
29
+ def restart
30
+ puts 'Restarting PostgreSQL...'
31
+
32
+ # Restart on all hosts
33
+ config.all_hosts.each do |host|
34
+ ssh_executor.restart_postgres(host, config.version)
35
+ end
36
+ end
37
+
38
+ # Public method to create application users - called after repmgr setup
39
+ # This is done after repmgr to avoid being wiped by cluster recreation
40
+ def create_application_users
41
+ return unless config.app_user && config.app_database
42
+
43
+ puts "\nšŸ“ Creating application users and databases..."
44
+ create_app_user_and_database(config.primary_host)
45
+ end
46
+
47
+ private
48
+
49
+ def install_on_host(host, is_primary:)
50
+ puts " Installing on #{host}..."
51
+
52
+ ssh_executor.install_postgres(host, config.version)
53
+ ssh_executor.ensure_cluster_exists(host, config.version)
54
+
55
+ # Get base component config
56
+ component_config = config.component_config(:core)
57
+
58
+ # Calculate optimal PostgreSQL settings if performance tuning is enabled
59
+ # pg_config is used in ERB template via binding
60
+ pg_config = if config.component_enabled?(:performance_tuning)
61
+ calculate_tuned_settings(host, component_config)
62
+ else
63
+ component_config[:postgresql] || {}
64
+ end
65
+ _ = pg_config # Used in ERB template
66
+
67
+ upload_template(host, 'postgresql.conf.erb', "/etc/postgresql/#{config.version}/main/postgresql.conf", binding,
68
+ owner: 'postgres:postgres')
69
+ upload_template(host, 'pg_hba.conf.erb', "/etc/postgresql/#{config.version}/main/pg_hba.conf", binding,
70
+ owner: 'postgres:postgres')
71
+
72
+ ssh_executor.restart_postgres(host, config.version)
73
+ end
74
+
75
+ def calculate_tuned_settings(host, component_config)
76
+ tuning_config = config.component_config(:performance_tuning)
77
+ db_type = tuning_config[:db_type] || 'web'
78
+
79
+ puts " Auto-tuning PostgreSQL for #{db_type} workload..."
80
+
81
+ # Initialize tuner and calculate optimal settings
82
+ tuner = PerformanceTuner.new(config, ssh_executor)
83
+ optimal_settings = tuner.tune_for_host(host, db_type: db_type)
84
+
85
+ # Merge: user config overrides calculated settings
86
+ user_postgresql = component_config[:postgresql] || {}
87
+ optimal_settings.merge(user_postgresql)
88
+ end
89
+
90
+ def install_packages_only(host)
91
+ puts " Installing packages on #{host} (cluster will be created by repmgr)..."
92
+ ssh_executor.install_postgres(host, config.version)
93
+ end
94
+
95
+ def create_app_user_and_database(host)
96
+ app_user = config.app_user
97
+ app_database = config.app_database
98
+
99
+ return unless app_user && app_database
100
+
101
+ puts " Creating application user '#{app_user}' and database '#{app_database}'..."
102
+ app_password = resolve_app_password
103
+ sql = build_app_user_sql(app_user, app_database, app_password)
104
+
105
+ ssh_executor.execute_on_host(host) do
106
+ upload! StringIO.new(sql), '/tmp/create_app_user.sql'
107
+ execute :chmod, '644', '/tmp/create_app_user.sql'
108
+ execute :sudo, '-u', 'postgres', 'psql', '-f', '/tmp/create_app_user.sql'
109
+ execute :rm, '-f', '/tmp/create_app_user.sql'
110
+
111
+ puts " āœ“ Created app user '#{app_user}' and database '#{app_database}'"
112
+ end
113
+ rescue StandardError => e
114
+ warn " Warning: Could not create app user: #{e.message}"
115
+ warn ' You may need to create the user manually'
116
+ end
117
+
118
+ def resolve_app_password
119
+ app_password = secrets.resolve('app_password')
120
+ if app_password.nil? || app_password.empty?
121
+ raise Error, 'app_password is empty or nil. Check your postgres.yml secrets section and ensure RAILS_ENV=production is set.'
122
+ end
123
+
124
+ app_password
125
+ rescue StandardError => e
126
+ raise Error, "Cannot resolve app_password: #{e.message}. Make sure RAILS_ENV is set when running deployment."
127
+ end
128
+
129
+ def build_app_user_sql(app_user, app_database, app_password)
130
+ escaped_password = app_password.gsub("'", "''")
131
+
132
+ [
133
+ '-- Create app user if not exists',
134
+ 'DO $$',
135
+ 'BEGIN',
136
+ " IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '#{app_user}') THEN",
137
+ " CREATE USER #{app_user} WITH PASSWORD '#{escaped_password}' CREATEDB;",
138
+ ' ELSE',
139
+ " ALTER USER #{app_user} WITH PASSWORD '#{escaped_password}';",
140
+ ' END IF;',
141
+ 'END $$;',
142
+ '',
143
+ '-- Ensure user has CREATEDB',
144
+ "ALTER USER #{app_user} CREATEDB;",
145
+ '',
146
+ '-- Create database if not exists',
147
+ "SELECT 'CREATE DATABASE #{app_database} OWNER #{app_user}'",
148
+ "WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = '#{app_database}')\\gexec",
149
+ '',
150
+ '-- Grant privileges',
151
+ "GRANT ALL PRIVILEGES ON DATABASE #{app_database} TO #{app_user};",
152
+ "\\c #{app_database}",
153
+ "GRANT ALL ON SCHEMA public TO #{app_user};"
154
+ ].join("\n")
155
+ end
156
+ end
157
+ end
158
+ end