files/gitlab-cookbooks/consul/libraries/consul_helper.rb (199 lines of code) (raw):

require 'timeout' class ConsulHelper attr_reader :node, :default_configuration, :default_server_configuration # List of existing services that we provide configuration for consul monitoring # # When adding a new service to consul, add to the constant below and make sure you # provide an `enable_service_#{service_name}` and `disable_service_#{service_name}` recipe SERVICES = %w(postgresql).freeze # This version should be keep in sync with consul versions in # software/consul.rb and consul_download.rb. SUPPORTED_MINOR = '1.18.2'.freeze def initialize(node) @node = node @default_configuration = { 'client_addr' => nil, 'datacenter' => 'gitlab_consul', 'disable_update_check' => true, 'enable_script_checks' => false, 'enable_local_script_checks' => true, 'node_name' => node['consul']['node_name'] || node['fqdn'], 'rejoin_after_leave' => true, 'server' => false, } .merge(encryption_configuration) .merge(ports_configuration) .merge(tls_configuration) @default_server_configuration = { 'bootstrap_expect' => 3 } end def server? !!node['consul']['configuration']['server'] end def use_tls? node['consul']['use_tls'] end def tls_configuration return {} unless use_tls? verify_incoming = node['consul']['tls_verify_client'] tls_cfg = { 'tls' => { 'defaults' => { 'ca_file' => node['consul']['tls_ca_file'], 'cert_file' => node['consul']['tls_certificate_file'], 'key_file' => node['consul']['tls_key_file'], 'verify_outgoing' => true, 'verify_incoming' => verify_incoming.nil? ? server? : verify_incoming }.compact } } tls_cfg['ports'] = { 'https': api_port('https') } tls_cfg end def final_config config = Chef::Mixin::DeepMerge.merge( default_configuration, node['consul']['configuration'] ).select { |k, v| !v.nil? } if server? return Chef::Mixin::DeepMerge.merge( default_server_configuration, config ) end config end def configuration final_config.to_json end def use_encryption? encryption_key = node['consul']['encryption_key'] !encryption_key.nil? && !encryption_key.empty? end def encryption_configuration return {} unless use_encryption? { 'encrypt' => node['consul']['encryption_key'], 'encrypt_verify_incoming' => node['consul']['encryption_verify_incoming'], 'encrypt_verify_outgoing' => node['consul']['encryption_verify_outgoing'] }.compact end def ports_configuration http_port = node['consul']['http_port'] https_port = node['consul']['https_port'] ports = {} ports['http'] = http_port unless http_port.nil? ports['https'] = https_port unless https_port.nil? { 'ports' => ports } end def api_url(scheme: nil) scheme ||= use_tls? || api_port('http').negative? ? 'https' : 'http' "#{scheme}://#{api_address(scheme)}:#{api_port(scheme)}" end def api_port(scheme) default_port = { 'http' => 8500, 'https' => 8501 } config = Chef::Mixin::DeepMerge.merge( ports_configuration, node['consul']['configuration']) config.dig('ports', scheme) || default_port[scheme] end def api_address(scheme) default_address = 'localhost' config_address = node.dig('consul', 'configuration', 'addresses', scheme) || node.dig('consul', 'configuration', 'client_addr') config_address.nil? || IPAddr.new(config_address).to_i.zero? ? default_address : config_address rescue IPAddr::InvalidAddressError # Have a best try when config address is invalid IP, such as a list of addresses default_address end def postgresql_service_config return node['consul']['service_config']['postgresql'] || {} unless node['consul']['service_config'].nil? ha_solution = postgresql_ha_solution { 'service' => { 'name' => node['consul']['internal']['postgresql_service_name'], 'address' => '', 'port' => node['postgresql']['port'], 'check' => { 'id': "service:#{node['consul']['internal']['postgresql_service_name']}", 'interval' => node['consul']['internal']['postgresql_service_check_interval'], 'status': node['consul']['internal']['postgresql_service_check_status'], 'args': node['consul']['internal']["postgresql_service_check_args_#{ha_solution}"] } } } end def postgresql_ha_solution return 'patroni_standby_cluster' if node['patroni'].key?('standby_cluster') && node['patroni']['standby_cluster']['enable'] 'patroni' end # Return a list of enabled services # # @return [Array] list of enabled services def enabled_services node['consul']['services'] end # Return a list of disabled services # # The list is generated by intersecting the existing services with the list of enabled # # @return [Array] list of services that are disabled def disabled_services SERVICES - node['consul']['services'] end def installed_version return unless OmnibusHelper.new(@node).service_up?('consul') command = "#{@node['consul']['binary_path']} version" command_output = VersionHelper.version(command) raise "Execution of the command `#{command}` failed" unless command_output version_match = command_output.match(/Consul v(?<consul_version>\d*\.\d*\.\d*)/) raise "Execution of the command `#{command}` generated unexpected output `#{command_output.strip}`" unless version_match version_match['consul_version'] end def running_version return unless OmnibusHelper.new(@node).service_up?('consul') response_code, response_body = get_api('/v1/agent/self') info = response_code == '200' ? JSON.parse(response_body, symbolize_names: true) : {} info[:Config][:Version] unless info.empty? end def installed_is_supported? installed = installed_version return true if installed.nil? major_installed, minor_installed = installed.split('.')[0..1] major_supported, minor_supported = SUPPORTED_MINOR.split('.') major_installed == major_supported && minor_installed == minor_supported end private def verify_incoming? final_config['tls']['defaults']['verify_incoming'] end def can_access_api_over_https? # If daemon isn't listening over HTTPS, no point in proceeding. Just use # HTTP. return false unless use_tls? # If incoming requests aren't verified, we can access API over HTTPS # without client certificates. return true unless verify_incoming? # If incoming connections are verified, we need a certificate/key to use as # "client" certificate/key while accessing API. Let's use the ones # specified as server certificate/key for this purpose. File.exist?(final_config['tls']['defaults']['cert_file'].to_s) && File.exist?(final_config['tls']['defaults']['key_file'].to_s) end def get_tls_args args = { use_ssl: true } return args unless verify_incoming? args[:cert] = OpenSSL::X509::Certificate.new(File.read(final_config['tls']['defaults']['cert_file'])) args[:key] = OpenSSL::PKey.read(File.read(final_config['tls']['defaults']['key_file'])) args end def get_api(endpoint, header = nil) if can_access_api_over_https? uri = URI(api_url(scheme: 'https')) args = get_tls_args else uri = URI(api_url(scheme: 'http')) args = {} end begin fetch_response(uri, endpoint, args, header) rescue Timeout::Error => e # If we were already using HTTP, there is nothing more we can do. # Fail hard. raise Timeout::Error, e.message if uri.scheme == "http" # We were trying HTTPS but, it wasn't available. Maybe HTTPS was enabled # in this reconfigure run, and won't be active till user restarts Consul, # and thus Consul is still running over HTTP only. Try accessing API over # HTTP. uri = URI(api_url(scheme: 'http')) args = {} fetch_response(uri, endpoint, args, header) end end def fetch_response(uri, endpoint, args, header = nil) Timeout.timeout(30, Timeout::Error, "Timed out waiting for Consul to start") do loop do Net::HTTP.start(uri.host, uri.port, **args) do |http| http.request_get(endpoint, header) do |response| return response.code, response.body end end rescue Errno::ECONNREFUSED, Errno::EADDRNOTAVAIL sleep 1 next else break end end end end