require 'singleton' require 'pathname' class SpecConfig include Singleton # NB: constructor should not do I/O as SpecConfig may be used by tests # only loading the lite spec helper. Do I/O eagerly in accessor methods. def initialize @uri_options = {} if ENV['MONGODB_URI'] @mongodb_uri = Mongo::URI.new(ENV['MONGODB_URI']) @uri_options = Mongo::Options::Mapper.transform_keys_to_symbols(@mongodb_uri.uri_options) if @uri_options[:replica_set] @addresses = @mongodb_uri.servers @connect_options = { connect: :replica_set, replica_set: @uri_options[:replica_set] } elsif @uri_options[:connect] == :sharded || ENV['TOPOLOGY'] == 'sharded-cluster' @addresses = @mongodb_uri.servers @connect_options = { connect: :sharded } elsif @uri_options[:connect] == :direct @addresses = @mongodb_uri.servers @connect_options = { connect: :direct } end if @uri_options[:ssl].nil? @ssl = (ENV['SSL'] == 'ssl') || (ENV['SSL_ENABLED'] == 'true') else @ssl = @uri_options[:ssl] end elsif ENV['MONGODB_ADDRESSES'] @addresses = ENV['MONGODB_ADDRESSES'] ? ENV['MONGODB_ADDRESSES'].split(',').freeze : [ '127.0.0.1:27017' ].freeze if ENV['RS_ENABLED'] @connect_options = { connect: :replica_set, replica_set: ENV['RS_NAME'] } elsif ENV['SHARDED_ENABLED'] @connect_options = { connect: :sharded } else @connect_options = { connect: :direct } end end @uri_tls_options = {} @uri_options.each do |k, v| k = k.to_s.downcase if k.start_with?('ssl') @uri_tls_options[k] = v end end @ssl ||= false end attr_reader :uri_options, :connect_options def addresses @addresses ||= begin if @mongodb_uri @mongodb_uri.servers else client = Mongo::Client.new(['localhost:27017'], server_selection_timeout: 5.02) begin client.cluster.next_primary @addresses = client.cluster.servers_list.map do |server| server.address.to_s end ensure client.close end end end end def connect_options @connect_options ||= begin # Discover deployment topology. # TLS options need to be merged for evergreen due to # https://github.com/10gen/mongo-orchestration/issues/268 client = Mongo::Client.new(addresses, Mongo::Options::Redacted.new( server_selection_timeout: 5.03, ).merge(ssl_options)) begin case client.cluster.topology.class.name when /Replica/ { connect: :replica_set, replica_set: client.cluster.topology.replica_set_name } when /Sharded/ { connect: :sharded } when /Single/ { connect: :direct } when /Unknown/ raise "Could not detect topology because the test client failed to connect to MongoDB deployment" else raise "Weird topology #{client.cluster.topology}" end ensure client.close end end end # Environment def ci? %w(1 true yes).include?(ENV['CI']&.downcase) end def mri? !jruby? end def jruby? !!(RUBY_PLATFORM =~ /\bjava\b/) end def platform RUBY_PLATFORM end def stress? %w(1 true yes).include?(ENV['STRESS']&.downcase) end def fork? %w(1 true yes).include?(ENV['FORK']&.downcase) end # Test suite configuration def client_debug? %w(1 true yes).include?(ENV['MONGO_RUBY_DRIVER_CLIENT_DEBUG']&.downcase) end def drivers_tools? !!ENV['DRIVERS_TOOLS'] end def active_support? %w(1 true yes).include?(ENV['WITH_ACTIVE_SUPPORT']) end # What compressor to use, if any. def compressors uri_options[:compressors] end def retry_reads uri_option_or_env_var(:retry_reads, 'RETRY_READS') end def retry_writes uri_option_or_env_var(:retry_writes, 'RETRY_WRITES') end def uri_option_or_env_var(driver_option_symbol, env_var_key) case uri_options[driver_option_symbol] when true true when false false else case (ENV[env_var_key] || '').downcase when 'yes', 'true', 'on', '1' true when 'no', 'false', 'off', '0' false else nil end end end def retry_writes? if retry_writes == false false else # Current default is to retry writes true end end def ssl? @ssl end # Username, not user object def user @mongodb_uri && @mongodb_uri.credentials[:user] end def password @mongodb_uri && @mongodb_uri.credentials[:password] end def auth_source uri_options[:auth_source] end def connect_replica_set? connect_options[:connect] == :replica_set end def print_summary puts "Connection options: #{test_options}" client = ClientRegistry.instance.global_client('basic') client.cluster.next_primary puts <<-EOT Topology: #{client.cluster.topology.class} connect: #{connect_options[:connect]} EOT end # Derived data def any_port addresses.first.split(':')[1] || '27017' end def spec_root File.join(File.dirname(__FILE__), '..') end def ssl_certs_dir Pathname.new("#{spec_root}/support/certificates") end # TLS certificates & keys def local_client_key_path "#{ssl_certs_dir}/client.key" end def client_key_path if drivers_tools? && ENV['DRIVER_TOOLS_CLIENT_KEY_PEM'] ENV['DRIVER_TOOLS_CLIENT_KEY_PEM'] else local_client_key_path end end def local_client_cert_path "#{ssl_certs_dir}/client.crt" end def client_cert_path if drivers_tools? && ENV['DRIVER_TOOLS_CLIENT_CERT_PEM'] ENV['DRIVER_TOOLS_CLIENT_CERT_PEM'] else local_client_cert_path end end def local_client_pem_path "#{ssl_certs_dir}/client.pem" end def client_pem_path if drivers_tools? && ENV['DRIVER_TOOLS_CLIENT_CERT_KEY_PEM'] ENV['DRIVER_TOOLS_CLIENT_CERT_KEY_PEM'] else local_client_pem_path end end def client_x509_pem_path "#{ssl_certs_dir}/client-x509.pem" end def second_level_cert_path "#{ssl_certs_dir}/client-second-level.crt" end def second_level_key_path "#{ssl_certs_dir}/client-second-level.key" end def second_level_cert_bundle_path "#{ssl_certs_dir}/client-second-level-bundle.pem" end def local_client_encrypted_key_path "#{ssl_certs_dir}/client-encrypted.key" end def client_encrypted_key_path if drivers_tools? && ENV['DRIVER_TOOLS_CLIENT_KEY_ENCRYPTED_PEM'] ENV['DRIVER_TOOLS_CLIENT_KEY_ENCRYPTED_PEM'] else local_client_encrypted_key_path end end def client_encrypted_key_passphrase 'passphrase' end def local_ca_cert_path "#{ssl_certs_dir}/ca.crt" end def ca_cert_path if drivers_tools? && ENV['DRIVER_TOOLS_CA_PEM'] ENV['DRIVER_TOOLS_CA_PEM'] else local_ca_cert_path end end def multi_ca_path "#{ssl_certs_dir}/multi-ca.crt" end # The default test database for all specs. def test_db 'ruby-driver'.freeze end # AWS IAM user access key id def fle_aws_key ENV['MONGO_RUBY_DRIVER_AWS_KEY'] end # AWS IAM user secret access key def fle_aws_secret ENV['MONGO_RUBY_DRIVER_AWS_SECRET'] end # Region of AWS customer master key def fle_aws_region ENV['MONGO_RUBY_DRIVER_AWS_REGION'] end # Amazon resource name (ARN) of AWS customer master key def fle_aws_arn ENV['MONGO_RUBY_DRIVER_AWS_ARN'] end def mongocryptd_port if ENV['MONGO_RUBY_DRIVER_MONGOCRYPTD_PORT'] && !ENV['MONGO_RUBY_DRIVER_MONGOCRYPTD_PORT'].empty? then ENV['MONGO_RUBY_DRIVER_MONGOCRYPTD_PORT'].to_i else 27020 end end # Option hashes def auth_options if x509_auth? { auth_mech: uri_options[:auth_mech], auth_source: '$external', } else { user: user, password: password, }.tap do |options| if auth_source options[:auth_source] = auth_source end %i(auth_mech auth_mech_properties).each do |key| if uri_options[key] options[key] = uri_options[key] end end end end end def ssl_options if ssl? { ssl: true, ssl_verify: true, ssl_cert: client_cert_path, ssl_key: client_key_path, ssl_ca_cert: ca_cert_path, }.merge(Utils.underscore_hash(@uri_tls_options)) else {} end end def compressor_options if compressors {compressors: compressors} else {} end end def retry_writes_options {retry_writes: retry_writes?} end # Base test options. def base_test_options { # Automatic encryption tests require a minimum of three connections: # - The driver checks out a connection to build a command. # - It may need to encrypt the command, which could require a query to # the key vault collection triggered by libmongocrypt. # - If the key vault client has auto encryption options, it will also # attempt to encrypt this query, resulting in a third connection. # In the worst case using FLE may end up tripling the number of # connections that the driver uses at any one time. max_pool_size: 3, heartbeat_frequency: 20, # The test suite seems to perform a number of operations # requiring server selection. Hence a timeout of 1 here, # together with e.g. a misconfigured replica set, # means the test suite hangs for about 4 seconds before # failing. # Server selection timeout of 1 is insufficient for evergreen. server_selection_timeout: uri_options[:server_selection_timeout] || (ssl? ? 8.01 : 7.01), # Since connections are established under the wait queue timeout, # the wait queue timeout should be at least as long as the # connect timeout. wait_queue_timeout: 6.04, connect_timeout: 2.91, socket_timeout: 5.09, max_idle_time: 100.02, # Uncomment to have exceptions in background threads log complete # backtraces. #bg_error_backtrace: true, } end # Options for test suite clients. def test_options base_test_options.merge(connect_options). merge(ssl_options).merge(compressor_options).merge(retry_writes_options) end # TODO auth_options should probably be in test_options def all_test_options test_options.merge(auth_options) end def authorized_test_options test_options.merge(credentials_or_external_user( user: test_user.name, password: test_user.password, auth_source: auth_options[:auth_source], )) end # User objects # Gets the root system administrator user. def root_user Mongo::Auth::User.new( user: user || 'root-user', password: password || 'password', roles: [ Mongo::Auth::Roles::USER_ADMIN_ANY_DATABASE, Mongo::Auth::Roles::DATABASE_ADMIN_ANY_DATABASE, Mongo::Auth::Roles::READ_WRITE_ANY_DATABASE, Mongo::Auth::Roles::HOST_MANAGER, Mongo::Auth::Roles::CLUSTER_ADMIN ] ) end # Get the default test user for the suite on versions 2.6 and higher. def test_user Mongo::Auth::User.new( database: 'admin', user: 'ruby-test-user', password: 'password', roles: [ { role: Mongo::Auth::Roles::READ_WRITE, db: test_db }, { role: Mongo::Auth::Roles::DATABASE_ADMIN, db: test_db }, { role: Mongo::Auth::Roles::READ_WRITE, db: 'invalid_database' }, { role: Mongo::Auth::Roles::DATABASE_ADMIN, db: 'invalid_database' }, # For transactions examples { role: Mongo::Auth::Roles::READ_WRITE, db: 'hr' }, { role: Mongo::Auth::Roles::DATABASE_ADMIN, db: 'hr' }, { role: Mongo::Auth::Roles::READ_WRITE, db: 'reporting' }, { role: Mongo::Auth::Roles::DATABASE_ADMIN, db: 'reporting' }, # For spec tests { role: Mongo::Auth::Roles::READ_WRITE, db: 'crud-tests' }, { role: Mongo::Auth::Roles::DATABASE_ADMIN, db: 'crud-tests' }, { role: Mongo::Auth::Roles::READ_WRITE, db: 'crud-default' }, { role: Mongo::Auth::Roles::DATABASE_ADMIN, db: 'crud-default' }, { role: Mongo::Auth::Roles::READ_WRITE, db: 'default_write_concern_db' }, { role: Mongo::Auth::Roles::DATABASE_ADMIN, db: 'default_write_concern_db' }, { role: Mongo::Auth::Roles::READ_WRITE, db: 'retryable-reads-tests' }, { role: Mongo::Auth::Roles::DATABASE_ADMIN, db: 'retryable-reads-tests' }, { role: Mongo::Auth::Roles::READ_WRITE, db: 'sdam-tests' }, { role: Mongo::Auth::Roles::DATABASE_ADMIN, db: 'sdam-tests' }, { role: Mongo::Auth::Roles::READ_WRITE, db: 'transaction-tests' }, { role: Mongo::Auth::Roles::DATABASE_ADMIN, db: 'transaction-tests' }, { role: Mongo::Auth::Roles::READ_WRITE, db: 'withTransaction-tests' }, { role: Mongo::Auth::Roles::DATABASE_ADMIN, db: 'withTransaction-tests' }, { role: Mongo::Auth::Roles::READ_WRITE, db: 'admin' }, { role: Mongo::Auth::Roles::DATABASE_ADMIN, db: 'admin' }, ] ) end def x509_auth? uri_options[:auth_mech] == :mongodb_x509 end # When we authenticate with a username & password mechanism (scram, cr) # we create a variety of users in the test suite for different purposes. # When we authenticate with passwordless mechanisms (x509, aws) we use # the globally specified user for all operations. def external_user? case uri_options[:auth_mech] when :mongodb_x509, :aws true when nil, :scram, :scram256 false else raise "Unknown auth mechanism value: #{uri_options[:auth_mech]}" end end # When we use external authentication, omit all of the users we normally # create and authenticate with the external mechanism. This also ensures # our various helpers work correctly when the only users available are # the external ones. def credentials_or_external_user(creds) if external_user? auth_options else creds end end end