Class: ActivePostgres::Configuration

Inherits:
Object
  • Object
show all
Defined in:
lib/active_postgres/configuration.rb

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(config_hash, environment = 'development') ⇒ Configuration

Returns a new instance of Configuration.



8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# File 'lib/active_postgres/configuration.rb', line 8

def initialize(config_hash, environment = 'development')
  @environment = environment
  env_config = config_hash[environment] || {}

  @skip_deployment = env_config['skip_deployment'] == true

  @version = env_config['version'] || 18
  @user = env_config['user'] || 'ubuntu'
  @ssh_key = File.expand_path(env_config['ssh_key'] || '~/.ssh/id_rsa')
  @ssh_host_key_verification = normalize_ssh_host_key_verification(
    env_config['ssh_host_key_verification'] || env_config['ssh_verify_host_key']
  )

  @primary = env_config['primary'] || {}
  @standbys = env_config['standby'] || []
  @standbys = [@standbys] unless @standbys.is_a?(Array)

  @components = parse_components(env_config['components'] || {})
  @secrets_config = env_config['secrets'] || {}
end

Instance Attribute Details

#componentsObject (readonly)

Returns the value of attribute components.



5
6
7
# File 'lib/active_postgres/configuration.rb', line 5

def components
  @components
end

#database_configObject (readonly)

Returns the value of attribute database_config.



5
6
7
# File 'lib/active_postgres/configuration.rb', line 5

def database_config
  @database_config
end

#environmentObject (readonly)

Returns the value of attribute environment.



5
6
7
# File 'lib/active_postgres/configuration.rb', line 5

def environment
  @environment
end

#primaryObject (readonly)

Returns the value of attribute primary.



5
6
7
# File 'lib/active_postgres/configuration.rb', line 5

def primary
  @primary
end

#secrets_configObject (readonly)

Returns the value of attribute secrets_config.



5
6
7
# File 'lib/active_postgres/configuration.rb', line 5

def secrets_config
  @secrets_config
end

#ssh_host_key_verificationObject (readonly)

Returns the value of attribute ssh_host_key_verification.



5
6
7
# File 'lib/active_postgres/configuration.rb', line 5

def ssh_host_key_verification
  @ssh_host_key_verification
end

#ssh_keyObject (readonly)

Returns the value of attribute ssh_key.



5
6
7
# File 'lib/active_postgres/configuration.rb', line 5

def ssh_key
  @ssh_key
end

#standbysObject (readonly)

Returns the value of attribute standbys.



5
6
7
# File 'lib/active_postgres/configuration.rb', line 5

def standbys
  @standbys
end

#userObject (readonly)

Returns the value of attribute user.



5
6
7
# File 'lib/active_postgres/configuration.rb', line 5

def user
  @user
end

#versionObject (readonly)

Returns the value of attribute version.



5
6
7
# File 'lib/active_postgres/configuration.rb', line 5

def version
  @version
end

Class Method Details

.load(config_path = 'config/postgres.yml', environment = nil) ⇒ Object

Raises:



29
30
31
32
33
34
35
36
# File 'lib/active_postgres/configuration.rb', line 29

def self.load(config_path = 'config/postgres.yml', environment = nil)
  environment ||= ENV['BORING_ENVIRONMENT'] || ENV['RAILS_ENV'] || 'development'

  raise Error, "Config file not found: #{config_path}" unless File.exist?(config_path)

  config_hash = YAML.load_file(config_path, aliases: true)
  new(config_hash, environment)
end

Instance Method Details

#all_hostsObject



38
39
40
# File 'lib/active_postgres/configuration.rb', line 38

def all_hosts
  [primary_host] + standby_hosts
end

#app_databaseObject



204
205
206
207
# File 'lib/active_postgres/configuration.rb', line 204

def app_database
  value = component_config(:core)[:app_database]
  value.nil? || value.to_s.strip.empty? ? "app_#{environment}" : value
end

#app_userObject



199
200
201
202
# File 'lib/active_postgres/configuration.rb', line 199

def app_user
  value = component_config(:core)[:app_user]
  value.nil? || value.to_s.strip.empty? ? 'app' : value
end

#component_config(name) ⇒ Object



83
84
85
# File 'lib/active_postgres/configuration.rb', line 83

def component_config(name)
  @components[name] || {}
end

#component_enabled?(name) ⇒ Boolean

Returns:

  • (Boolean)


79
80
81
# File 'lib/active_postgres/configuration.rb', line 79

def component_enabled?(name)
  @components[name]&.[](:enabled) == true
end

#connection_host_for(host) ⇒ Object

Returns the host to use for direct PostgreSQL connections (private_ip preferred)



101
102
103
104
# File 'lib/active_postgres/configuration.rb', line 101

def connection_host_for(host)
  node = node_config_for(host)
  private_ip_for(node) || host
end

#node_label_for(host) ⇒ Object



114
115
116
117
118
119
120
# File 'lib/active_postgres/configuration.rb', line 114

def node_label_for(host)
  if host == primary_host
    @primary['label']
  else
    standby_config_for(host)&.dig('label')
  end
end

#pgbouncer_app_hostsObject



42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# File 'lib/active_postgres/configuration.rb', line 42

def pgbouncer_app_hosts
  hosts = component_config(:pgbouncer)[:app_hosts] || component_config(:pgbouncer)['app_hosts'] || []
  hosts = [hosts] unless hosts.is_a?(Array)

  hosts.filter_map do |entry|
    case entry
    when String
      entry.strip.empty? ? nil : { 'host' => entry.strip }
    when Hash
      host = entry['host'] || entry[:host] || entry['ip'] || entry[:ip]
      next if host.to_s.strip.empty?

      entry.merge('host' => host.to_s.strip)
    end
  end
end

#pgbouncer_primary_recordObject



59
60
61
62
63
64
65
66
67
68
69
# File 'lib/active_postgres/configuration.rb', line 59

def pgbouncer_primary_record
  pgbouncer = component_config(:pgbouncer)
  explicit = pgbouncer[:primary_record] || pgbouncer['primary_record']
  return explicit.to_s.strip unless explicit.to_s.strip.empty?

  dns_failover = component_config(:repmgr)[:dns_failover] || component_config(:repmgr)['dns_failover'] || {}
  record = dns_failover[:primary_record] || dns_failover['primary_record']
  return record.to_s.strip unless record.to_s.strip.empty?

  primary_replication_host
end

#pgbouncer_userObject



195
196
197
# File 'lib/active_postgres/configuration.rb', line 195

def pgbouncer_user
  component_config(:pgbouncer)[:user] || 'pgbouncer'
end

#postgres_userObject

Database and user configuration helpers from components



179
180
181
# File 'lib/active_postgres/configuration.rb', line 179

def postgres_user
  component_config(:core)[:postgres_user] || 'postgres'
end

#primary_connection_hostObject



106
107
108
# File 'lib/active_postgres/configuration.rb', line 106

def primary_connection_host
  connection_host_for(primary_host)
end

#primary_hostObject



71
72
73
# File 'lib/active_postgres/configuration.rb', line 71

def primary_host
  @primary['host']
end

#primary_replication_hostObject



91
92
93
# File 'lib/active_postgres/configuration.rb', line 91

def primary_replication_host
  replication_host_for(primary_host)
end

#replication_host_for(host) ⇒ Object



95
96
97
98
# File 'lib/active_postgres/configuration.rb', line 95

def replication_host_for(host)
  node = node_config_for(host)
  private_ip_for(node) || host
end

#replication_userObject



191
192
193
# File 'lib/active_postgres/configuration.rb', line 191

def replication_user
  component_config(:repmgr)[:replication_user] || 'replication'
end

#repmgr_databaseObject



187
188
189
# File 'lib/active_postgres/configuration.rb', line 187

def repmgr_database
  component_config(:repmgr)[:database] || 'repmgr'
end

#repmgr_userObject



183
184
185
# File 'lib/active_postgres/configuration.rb', line 183

def repmgr_user
  component_config(:repmgr)[:user] || 'repmgr'
end

#skip_deployment?Boolean

Returns:

  • (Boolean)


87
88
89
# File 'lib/active_postgres/configuration.rb', line 87

def skip_deployment?
  @skip_deployment
end

#standby_config_for(host) ⇒ Object



110
111
112
# File 'lib/active_postgres/configuration.rb', line 110

def standby_config_for(host)
  @standbys.find { |s| s['host'] == host }
end

#standby_hostsObject



75
76
77
# File 'lib/active_postgres/configuration.rb', line 75

def standby_hosts
  @standbys.map { |s| s['host'] }
end

#validate!Object

Raises:



122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
# File 'lib/active_postgres/configuration.rb', line 122

def validate!
  raise Error, 'No primary host defined' unless primary_host

  # Validate required secrets if components are enabled
  raise Error, 'Missing replication_password secret' if component_enabled?(:repmgr) && !secrets_config['replication_password']
  raise Error, 'Missing monitoring_password secret' if component_enabled?(:monitoring) && !secrets_config['monitoring_password']
  if component_enabled?(:monitoring)
    grafana_config = component_config(:monitoring)[:grafana] || {}
    if grafana_config[:enabled] && !secrets_config['grafana_admin_password']
      raise Error, 'Missing grafana_admin_password secret'
    end
    if grafana_config[:enabled] && grafana_config[:host].to_s.strip.empty?
      raise Error, 'monitoring.grafana.host is required when grafana is enabled'
    end
  end
  if component_enabled?(:pgbackrest)
    pg_config = component_config(:pgbackrest)
    retention_full = pg_config[:retention_full]
    retention_archive = pg_config[:retention_archive]
    if retention_full && retention_archive && retention_archive.to_i < retention_full.to_i
      raise Error, 'pgbackrest.retention_archive must be >= retention_full for PITR safety'
    end
  end

  if component_enabled?(:pgbouncer)
    pgbouncer_app_hosts.each do |app_host|
      host = app_host['host'] || app_host[:host]
      raise Error, 'pgbouncer.app_hosts entries must include host' if host.to_s.strip.empty?
    end
  end

    if component_enabled?(:repmgr)
      dns_failover = component_config(:repmgr)[:dns_failover]
      if dns_failover && dns_failover[:enabled]
        domains = Array(dns_failover[:domains] || dns_failover[:domain]).map(&:to_s).map(&:strip).reject(&:empty?)
        servers = Array(dns_failover[:dns_servers])
        provider = (dns_failover[:provider] || 'dnsmasq').to_s.strip

        raise Error, 'dns_failover.domain or dns_failover.domains is required when enabled' if domains.empty?
        raise Error, 'dns_failover.dns_servers is required when enabled' if servers.empty?
        raise Error, "Unsupported dns_failover provider '#{provider}'" unless provider == 'dnsmasq'

        servers.each do |server|
          next unless server.is_a?(Hash)

          ssh_host = server['ssh_host'] || server[:ssh_host] || server['host'] || server[:host]
          private_ip = server['private_ip'] || server[:private_ip] || server['ip'] || server[:ip]
          raise Error, 'dns_failover.dns_servers entries must include host/ssh_host or private_ip' if
            (ssh_host.nil? || ssh_host.to_s.strip.empty?) && (private_ip.nil? || private_ip.to_s.strip.empty?)
        end
      end
    end

  true
end