diff --git a/benchmarks/actions/002_info.rb b/benchmarks/actions/002_info.rb index ef99119d28..1b095ab02e 100644 --- a/benchmarks/actions/002_info.rb +++ b/benchmarks/actions/002_info.rb @@ -14,7 +14,6 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. - require_relative '../lib/benchmarks' Benchmarks.register \ diff --git a/elasticsearch-api/elasticsearch-api.gemspec b/elasticsearch-api/elasticsearch-api.gemspec index b5d9602099..e9aa5f48c3 100644 --- a/elasticsearch-api/elasticsearch-api.gemspec +++ b/elasticsearch-api/elasticsearch-api.gemspec @@ -49,7 +49,6 @@ Gem::Specification.new do |s| s.add_development_dependency 'ansi' s.add_development_dependency 'bundler' s.add_development_dependency 'elasticsearch' - s.add_development_dependency 'elasticsearch-transport' s.add_development_dependency 'minitest' s.add_development_dependency 'minitest-reporters' s.add_development_dependency 'mocha' diff --git a/elasticsearch-api/spec/spec_helper.rb b/elasticsearch-api/spec/spec_helper.rb index 0983297570..82a66f5bf3 100644 --- a/elasticsearch-api/spec/spec_helper.rb +++ b/elasticsearch-api/spec/spec_helper.rb @@ -28,7 +28,6 @@ require 'jbuilder' require 'jsonify' require 'elasticsearch' -require 'elasticsearch-transport' require 'elasticsearch-api' require 'ansi' diff --git a/elasticsearch-transport/lib/elasticsearch/transport.rb b/elasticsearch-transport/lib/elasticsearch/transport.rb index 696a469973..c142d8228f 100644 --- a/elasticsearch-transport/lib/elasticsearch/transport.rb +++ b/elasticsearch-transport/lib/elasticsearch/transport.rb @@ -35,15 +35,3 @@ require "elasticsearch/transport/redacted" require "elasticsearch/transport/version" - -module Elasticsearch - module Client - - # A convenience wrapper for {::Elasticsearch::Transport::Client#initialize}. - # - def new(arguments={}, &block) - Elasticsearch::Transport::Client.new(arguments, &block) - end - extend self - end -end diff --git a/elasticsearch-transport/lib/elasticsearch/transport/client.rb b/elasticsearch-transport/lib/elasticsearch/transport/client.rb index 12885aab65..bbaee5e849 100644 --- a/elasticsearch-transport/lib/elasticsearch/transport/client.rb +++ b/elasticsearch-transport/lib/elasticsearch/transport/client.rb @@ -49,11 +49,6 @@ class Client # @since 7.0.0 DEFAULT_HOST = 'localhost:9200'.freeze - # The default port to use if connecting using a Cloud ID. - # - # @since 7.2.0 - DEFAULT_CLOUD_PORT = 9243 - # The default port to use if not otherwise specified. # # @since 7.2.0 @@ -118,18 +113,13 @@ class Client # The default is false. Responses will automatically be inflated if they are compressed. # If a custom transport object is used, it must handle the request compression and response inflation. # - # @option api_key [String, Hash] :api_key Use API Key Authentication, either the base64 encoding of `id` and `api_key` - # joined by a colon as a String, or a hash with the `id` and `api_key` values. - # @option opaque_id_prefix [String] :opaque_id_prefix set a prefix for X-Opaque-Id when initializing the client. - # This will be prepended to the id you set before each request - # if you're using X-Opaque-Id # @option enable_meta_header [Boolean] :enable_meta_header Enable sending the meta data header to Cloud. # (Default: true) # # @yield [faraday] Access and configure the `Faraday::Connection` instance directly with a block # def initialize(arguments = {}, &block) - @options = arguments.each_with_object({}) { |(k, v), args| args[k.to_sym] = v } + @options = arguments.transform_keys(&:to_sym) @arguments = @options @arguments[:logger] ||= @arguments[:log] ? DEFAULT_LOGGER.call() : nil @arguments[:tracer] ||= @arguments[:trace] ? DEFAULT_TRACER.call() : nil @@ -139,21 +129,17 @@ def initialize(arguments = {}, &block) @arguments[:randomize_hosts] ||= false @arguments[:transport_options] ||= {} @arguments[:http] ||= {} - @arguments[:enable_meta_header] = arguments.fetch(:enable_meta_header) { true } + @arguments[:enable_meta_header] = arguments.fetch(:enable_meta_header, true) @options[:http] ||= {} - set_api_key if (@api_key = @arguments[:api_key]) - - @seeds = extract_cloud_creds(@arguments) - @seeds ||= __extract_hosts(@arguments[:hosts] || - @arguments[:host] || - @arguments[:url] || - @arguments[:urls] || - ENV['ELASTICSEARCH_URL'] || - DEFAULT_HOST) + @hosts = __extract_hosts(@arguments[:hosts] || + @arguments[:host] || + @arguments[:url] || + @arguments[:urls] || + ENV['ELASTICSEARCH_URL'] || + DEFAULT_HOST) @send_get_body_as = @arguments[:send_get_body_as] || 'GET' - @opaque_id_prefix = @arguments[:opaque_id_prefix] || nil if @arguments[:request_timeout] @arguments[:transport_options][:request] = { timeout: @arguments[:request_timeout] } @@ -166,38 +152,25 @@ def initialize(arguments = {}, &block) @transport = if @transport_class == Transport::HTTP::Faraday @arguments[:adapter] ||= __auto_detect_adapter set_meta_header # from include MetaHeader - @transport_class.new(hosts: @seeds, options: @arguments) do |faraday| + @transport_class.new(hosts: @hosts, options: @arguments) do |faraday| faraday.adapter(@arguments[:adapter]) block&.call faraday end else set_meta_header # from include MetaHeader - @transport_class.new(hosts: @seeds, options: @arguments) + @transport_class.new(hosts: @hosts, options: @arguments) end end end - # Performs a request through delegation to {#transport}. # def perform_request(method, path, params = {}, body = nil, headers = nil) method = @send_get_body_as if 'GET' == method && body - if (opaque_id = params.delete(:opaque_id)) - headers = {} if headers.nil? - opaque_id = @opaque_id_prefix ? "#{@opaque_id_prefix}#{opaque_id}" : opaque_id - headers.merge!('X-Opaque-Id' => opaque_id) - end transport.perform_request(method, path, params, body, headers) end private - def set_api_key - @api_key = __encode(@api_key) if @api_key.is_a? Hash - add_header('Authorization' => "ApiKey #{@api_key}") - @arguments.delete(:user) - @arguments.delete(:password) - end - def add_header(header) headers = @arguments[:transport_options]&.[](:headers) || {} headers.merge!(header) @@ -206,30 +179,6 @@ def add_header(header) ) end - def extract_cloud_creds(arguments) - return unless arguments[:cloud_id] && !arguments[:cloud_id].empty? - - name = arguments[:cloud_id].split(':')[0] - cloud_url, elasticsearch_instance = Base64.decode64(arguments[:cloud_id].gsub("#{name}:", '')).split('$') - - if cloud_url.include?(':') - url, port = cloud_url.split(':') - host = "#{elasticsearch_instance}.#{url}" - else - host = "#{elasticsearch_instance}.#{cloud_url}" - port = arguments[:port] || DEFAULT_CLOUD_PORT - end - [ - { - scheme: 'https', - user: arguments[:user], - password: arguments[:password], - host: host, - port: port.to_i - } - ] - end - # Normalizes and returns hosts configuration. # # Arrayifies the `hosts_config` argument and extracts `host` and `port` info from strings. @@ -328,13 +277,6 @@ def __auto_detect_adapter ::Faraday.default_adapter end end - - # Encode credentials for the Authorization Header - # Credentials is the base64 encoding of id and api_key joined by a colon - # @see https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-create-api-key.html - def __encode(api_key) - Base64.strict_encode64([api_key[:id], api_key[:api_key]].join(':')) - end end end end diff --git a/elasticsearch-transport/spec/elasticsearch/transport/base_spec.rb b/elasticsearch-transport/spec/elasticsearch/transport/base_spec.rb index 6fa7d3500a..c93c25061f 100644 --- a/elasticsearch-transport/spec/elasticsearch/transport/base_spec.rb +++ b/elasticsearch-transport/spec/elasticsearch/transport/base_spec.rb @@ -31,14 +31,14 @@ it 'does not include the password in the logged string' do expect(logger).not_to receive(:error).with(/secret_password/) expect { - client.cluster.stats + client.perform_request('GET', '/_cluster/stats') }.to raise_exception(Faraday::ConnectionFailed) end it 'replaces the password with the string \'REDACTED\'' do expect(logger).to receive(:error).with(/REDACTED/) expect { - client.cluster.stats + client.perform_request('GET', '/_cluster/stats') }.to raise_exception(Faraday::ConnectionFailed) end end @@ -93,7 +93,7 @@ end it 'raises an exception' do - expect { client.info }.to raise_exception(Faraday::ConnectionFailed) + expect { client.perform_request('GET', '/info') }.to raise_exception(Faraday::ConnectionFailed) end end diff --git a/elasticsearch-transport/spec/elasticsearch/transport/client_spec.rb b/elasticsearch-transport/spec/elasticsearch/transport/client_spec.rb index cea79bbdac..fa731f4f5f 100644 --- a/elasticsearch-transport/spec/elasticsearch/transport/client_spec.rb +++ b/elasticsearch-transport/spec/elasticsearch/transport/client_spec.rb @@ -24,10 +24,6 @@ end end - it 'is aliased as Elasticsearch::Client' do - expect(Elasticsearch::Client.new).to be_a(described_class) - end - it 'has a default transport' do expect(client.transport).to be_a(Elasticsearch::Transport::Client::DEFAULT_TRANSPORT_CLASS) end @@ -70,48 +66,6 @@ end end - context 'when an encoded api_key is provided' do - let(:client) do - described_class.new(api_key: 'an_api_key') - end - let(:authorization_header) do - client.transport.connections.first.connection.headers['Authorization'] - end - - it 'Adds the ApiKey header to the connection' do - expect(authorization_header).to eq('ApiKey an_api_key') - end - end - - context 'when an un-encoded api_key is provided' do - let(:client) do - described_class.new(api_key: { id: 'my_id', api_key: 'my_api_key' }) - end - let(:authorization_header) do - client.transport.connections.first.connection.headers['Authorization'] - end - - it 'Adds the ApiKey header to the connection' do - expect(authorization_header).to eq("ApiKey #{Base64.strict_encode64('my_id:my_api_key')}") - end - end - - context 'when basic auth and api_key are provided' do - let(:client) do - described_class.new( - api_key: { id: 'my_id', api_key: 'my_api_key' }, - host: 'http://elastic:password@localhost:9200' - ) - end - let(:authorization_header) do - client.transport.connections.first.connection.headers['Authorization'] - end - - it 'removes basic auth credentials' do - expect(authorization_header).not_to match(/^Basic/) - expect(authorization_header).to match(/^ApiKey/) - end - end context 'when a user-agent header is specified as client option in lower-case' do @@ -327,158 +281,6 @@ end end - context 'when cloud credentials are provided' do - let(:client) do - described_class.new( - cloud_id: 'name:bG9jYWxob3N0JGFiY2QkZWZnaA==', - user: 'elastic', - password: 'changeme' - ) - end - - let(:hosts) do - client.transport.hosts - end - - it 'extracts the cloud credentials' do - expect(hosts[0][:host]).to eq('abcd.localhost') - expect(hosts[0][:protocol]).to eq('https') - expect(hosts[0][:user]).to eq('elastic') - expect(hosts[0][:password]).to eq('changeme') - expect(hosts[0][:port]).to eq(9243) - end - - it 'creates the correct full url' do - expect( - client.transport.__full_url(client.transport.hosts[0]) - ).to eq('https://elastic:changeme@abcd.localhost:9243') - end - - context 'when a port is specified' do - let(:client) do - described_class.new( - cloud_id: 'name:bG9jYWxob3N0JGFiY2QkZWZnaA==', - user: 'elastic', - password: 'changeme', - port: 9250 - ) - end - - it 'sets the specified port along with the cloud credentials' do - expect(hosts[0][:host]).to eq('abcd.localhost') - expect(hosts[0][:protocol]).to eq('https') - expect(hosts[0][:user]).to eq('elastic') - expect(hosts[0][:password]).to eq('changeme') - expect(hosts[0][:port]).to eq(9250) - end - - it 'creates the correct full url' do - expect(client.transport.__full_url(client.transport.hosts[0])).to eq('https://elastic:changeme@abcd.localhost:9250') - end - end - - context 'when the cluster has alternate names' do - let(:client) do - described_class.new( - cloud_id: 'myCluster:bG9jYWxob3N0JGFiY2QkZWZnaA==', - user: 'elasticfantastic', - password: 'tobechanged' - ) - end - - let(:hosts) do - client.transport.hosts - end - - it 'extracts the cloud credentials' do - expect(hosts[0][:host]).to eq('abcd.localhost') - expect(hosts[0][:protocol]).to eq('https') - expect(hosts[0][:user]).to eq('elasticfantastic') - expect(hosts[0][:password]).to eq('tobechanged') - expect(hosts[0][:port]).to eq(9243) - end - - it 'creates the correct full url' do - expect( - client.transport.__full_url(client.transport.hosts[0]) - ).to eq('https://elasticfantastic:tobechanged@abcd.localhost:9243') - end - end - - context 'when decoded cloud id has a trailing dollar sign' do - let(:client) do - described_class.new( - cloud_id: 'a_cluster:bG9jYWxob3N0JGFiY2Qk', - user: 'elasticfantastic', - password: 'changeme' - ) - end - - let(:hosts) do - client.transport.hosts - end - - it 'extracts the cloud credentials' do - expect(hosts[0][:host]).to eq('abcd.localhost') - expect(hosts[0][:protocol]).to eq('https') - expect(hosts[0][:user]).to eq('elasticfantastic') - expect(hosts[0][:password]).to eq('changeme') - expect(hosts[0][:port]).to eq(9243) - end - - it 'creates the correct full url' do - expect( - client.transport.__full_url(client.transport.hosts[0]) - ).to eq('https://elasticfantastic:changeme@abcd.localhost:9243') - end - end - - context 'when the cloud host provides a port' do - let(:client) do - described_class.new( - cloud_id: 'name:ZWxhc3RpY19zZXJ2ZXI6OTI0MyRlbGFzdGljX2lk', - user: 'elastic', - password: 'changeme' - ) - end - - let(:hosts) do - client.transport.hosts - end - - it 'creates the correct full url' do - expect(hosts[0][:host]).to eq('elastic_id.elastic_server') - expect(hosts[0][:protocol]).to eq('https') - expect(hosts[0][:user]).to eq('elastic') - expect(hosts[0][:password]).to eq('changeme') - expect(hosts[0][:port]).to eq(9243) - end - end - - context 'when the cloud host provides a port and the port is also specified' do - let(:client) do - described_class.new( - cloud_id: 'name:ZWxhc3RpY19zZXJ2ZXI6OTI0MyRlbGFzdGljX2lk', - user: 'elastic', - password: 'changeme', - port: 9200 - ) - end - - let(:hosts) do - client.transport.hosts - end - - it 'creates the correct full url' do - expect(hosts[0][:host]).to eq('elastic_id.elastic_server') - expect(hosts[0][:protocol]).to eq('https') - expect(hosts[0][:user]).to eq('elastic') - expect(hosts[0][:password]).to eq('changeme') - expect(hosts[0][:port]).to eq(9243) - end - end - end - shared_examples_for 'a client that extracts hosts' do context 'when the host is a String' do context 'when there is a protocol specified' do @@ -553,7 +355,6 @@ end context 'when there is a port specified \'http://myhost:7101\'' do - let(:host) do 'http://myhost:7101' end @@ -571,7 +372,6 @@ end context 'when there is a path specified \'http://myhost:7101/api\'' do - let(:host) do 'http://myhost:7101/api' end @@ -603,9 +403,7 @@ end context 'when the protocol is https' do - context 'when there is no port specified \'https://myhost\'' do - let(:host) do 'https://myhost' end @@ -624,7 +422,6 @@ end context 'when there is a port specified \'https://myhost:7101\'' do - let(:host) do 'https://myhost:7101' end @@ -1026,7 +823,6 @@ end context 'when the hosts is invalid' do - let(:host) do 123 end @@ -1040,7 +836,6 @@ end context 'when hosts are specified with the \'host\' key' do - let(:client) do described_class.new(host: ['host1', 'host2', 'host3', 'host4'], randomize_hosts: true) end @@ -1055,7 +850,6 @@ end context 'when hosts are specified with the \'host\' key as a String' do - let(:client) do described_class.new('host' => ['host1', 'host2', 'host3', 'host4'], 'randomize_hosts' => true) end @@ -1070,7 +864,6 @@ end context 'when hosts are specified with the \'hosts\' key' do - let(:client) do described_class.new(hosts: host) end @@ -1083,7 +876,6 @@ end context 'when hosts are specified with the \'hosts\' key as a String' do - let(:client) do described_class.new('hosts' => host) end @@ -1096,7 +888,6 @@ end context 'when hosts are specified with the \'url\' key' do - let(:client) do described_class.new(url: host) end @@ -1109,7 +900,6 @@ end context 'when hosts are specified with the \'url\' key as a String' do - let(:client) do described_class.new('url' => host) end @@ -1122,7 +912,6 @@ end context 'when hosts are specified with the \'urls\' key' do - let(:client) do described_class.new(urls: host) end @@ -1135,7 +924,6 @@ end context 'when hosts are specified with the \'urls\' key as a String' do - let(:client) do described_class.new('urls' => host) end @@ -1332,7 +1120,6 @@ end context 'when \'request_timeout\' is defined' do - let(:client) do described_class.new(request_timeout: 120) end @@ -1343,7 +1130,6 @@ end context 'when \'request_timeout\' is defined as a String key' do - let(:client) do described_class.new('request_timeout' => 120) end @@ -1355,7 +1141,6 @@ end describe '#perform_request' do - let(:transport_instance) do Class.new { def initialize(*); end }.new end @@ -1370,15 +1155,17 @@ end context 'when the \'send_get_body_as\' option is specified' do - let(:client) do described_class.new(transport: transport_instance, :send_get_body_as => 'POST') end before do - expect(transport_instance).to receive(:perform_request).with('POST', '/', {}, - '{"foo":"bar"}', - '{"Content-Type":"application/x-ndjson"}').and_return(true) + expect(transport_instance).to receive(:perform_request) + .with( + 'POST', '/', {}, + '{"foo":"bar"}', + '{"Content-Type":"application/x-ndjson"}' + ).and_return(true) end let(:request) do @@ -1390,40 +1177,6 @@ end end - context 'when x-opaque-id is set' do - let(:client) { described_class.new(host: hosts) } - - it 'uses x-opaque-id on a request' do - expect(client.perform_request('GET', '/', { opaque_id: '12345' }).headers['x-opaque-id']).to eq('12345') - end - end - - context 'when an x-opaque-id prefix is set on initialization' do - let(:prefix) { 'elastic_cloud' } - let(:client) do - described_class.new(host: hosts, opaque_id_prefix: prefix) - end - - it 'uses x-opaque-id on a request' do - expect(client.perform_request('GET', '/', { opaque_id: '12345' }).headers['x-opaque-id']).to eq("#{prefix}12345") - end - - context 'when using an API call' do - let(:client) { described_class.new(host: hosts) } - - it 'doesnae raise an ArgumentError' do - expect { client.search(opaque_id: 'no_error') }.not_to raise_error - end - - it 'uses X-Opaque-Id in the header' do - allow(client).to receive(:perform_request) { OpenStruct.new(body: '') } - expect { client.search(opaque_id: 'opaque_id') }.not_to raise_error - expect(client).to have_received(:perform_request) - .with('GET', '_search', { opaque_id: 'opaque_id' }, nil, {}) - end - end - end - context 'when Elasticsearch response includes a warning header' do let(:client) do Elasticsearch::Transport::Client.new(hosts: hosts) @@ -1449,39 +1202,6 @@ end end end - - context 'when a header is set on an endpoint request' do - let(:client) { described_class.new(host: hosts) } - let(:headers) { { 'user-agent' => 'my ruby app' } } - - it 'performs the request with the header' do - allow(client).to receive(:perform_request) { OpenStruct.new(body: '') } - expect { client.search(headers: headers) }.not_to raise_error - expect(client).to have_received(:perform_request) - .with('GET', '_search', {}, nil, headers) - end - end - - context 'when a header is set on an endpoint request and on initialization' do - let!(:client) do - described_class.new( - host: hosts, - transport_options: { headers: instance_headers } - ) - end - let(:instance_headers) { { set_in_instantiation: 'header value' } } - let(:param_headers) {{'user-agent' => 'My Ruby Tests', 'set-on-method-call' => 'header value'}} - - it 'performs the request with the header' do - expected_headers = client.transport.connections.connections.first.connection.headers.merge(param_headers) - - expect_any_instance_of(Faraday::Connection) - .to receive(:run_request) - .with(:get, "http://#{hosts[0]}/_search", nil, expected_headers) { OpenStruct.new(body: '')} - - client.search(headers: param_headers) - end - end end context 'when the client connects to Elasticsearch' do diff --git a/elasticsearch-transport/spec/elasticsearch/transport/meta_header_spec.rb b/elasticsearch-transport/spec/elasticsearch/transport/meta_header_spec.rb index c70086f930..9eca75b85c 100644 --- a/elasticsearch-transport/spec/elasticsearch/transport/meta_header_spec.rb +++ b/elasticsearch-transport/spec/elasticsearch/transport/meta_header_spec.rb @@ -78,22 +78,6 @@ def meta_version end end - context 'when using API Key' do - let(:client) do - described_class.new(api_key: 'an_api_key', adapter: adapter) - end - - let(:authorization_header) do - client.transport.connections.first.connection.headers['Authorization'] - end - - it 'adds the ApiKey header to the connection' do - expect(authorization_header).to eq('ApiKey an_api_key') - expect(subject['x-elastic-client-meta']).to match(regexp) - expect(subject).to include('x-elastic-client-meta' => meta_header) - end - end - context 'adapters' do let(:meta_header) do if jruby? @@ -258,8 +242,8 @@ class MyTransport include Elasticsearch::Transport::Transport::Base def initialize(args); end end - let(:client) { Elasticsearch::Client.new(transport_class: MyTransport) } - let(:subject){ client.instance_variable_get("@arguments")[:transport_options][:headers] } + let(:client) { Elasticsearch::Transport::Client.new(transport_class: MyTransport) } + let(:subject) { client.instance_variable_get('@arguments')[:transport_options][:headers] } let(:meta_header) do if jruby? "es=#{meta_version},rb=#{RUBY_VERSION},t=#{Elasticsearch::Transport::VERSION},jv=#{ENV_JAVA['java.version']},jr=#{JRUBY_VERSION}" diff --git a/elasticsearch-xpack/lib/elasticsearch/xpack.rb b/elasticsearch-xpack/lib/elasticsearch/xpack.rb index 9255e1629d..b7d45ac5e0 100644 --- a/elasticsearch-xpack/lib/elasticsearch/xpack.rb +++ b/elasticsearch-xpack/lib/elasticsearch/xpack.rb @@ -44,128 +44,117 @@ class Client Elasticsearch::API::COMMON_PARAMS.push :job_id, :datafeed_id, :filter_id, :snapshot_id, :category_id, :policy_id module Elasticsearch - module Transport - class Client - # When a method is called on the client, if it's one of the xpack root - # namespace methods, send them to the xpack client. - # E.g.: client.xpack.usage => client.usage - # Excluding `info` since OSS and XPACK both have info endpoints. - TOP_LEVEL_METHODS = [ - :open_point_in_time, - :close_point_in_time, - :terms_enum, - :usage - ].freeze - - def method_missing(method, *args, &block) - return xpack.send(method, *args, &block) if TOP_LEVEL_METHODS.include?(method) - - super - end + class Client + # When a method is called on the client, if it's one of the xpack root + # namespace methods, send them to the xpack client. + # E.g.: client.xpack.usage => client.usage + # Excluding `info` since OSS and XPACK both have info endpoints. + TOP_LEVEL_METHODS = [:open_point_in_time, :close_point_in_time, :terms_enum, :usage].freeze - def respond_to_missing?(method_name, *args) - TOP_LEVEL_METHODS.include?(method_name) || super + TOP_LEVEL_METHODS.each do |method| + define_method :"#{method}" do |value| + xpack.send(method, *[value]) end + end - def xpack - @xpack ||= Elasticsearch::XPack::API::Client.new(self) - end + def xpack + @xpack ||= Elasticsearch::XPack::API::Client.new(self) + end - def security - @security ||= xpack.security - end + def security + @security ||= xpack.security + end - def ml - @ml ||= xpack.ml - end + def ml + @ml ||= xpack.ml + end - def rollup - @rollup ||= xpack.rollup - end + def rollup + @rollup ||= xpack.rollup + end - def watcher - @watcher ||= xpack.watcher - end + def watcher + @watcher ||= xpack.watcher + end - def graph - @graph ||= xpack.graph - end + def graph + @graph ||= xpack.graph + end - def migration - @migration ||= xpack.migration - end + def migration + @migration ||= xpack.migration + end - def sql - @sql ||= xpack.sql - end + def sql + @sql ||= xpack.sql + end - def deprecation - @deprecation ||= xpack.deprecation - end + def deprecation + @deprecation ||= xpack.deprecation + end - def data_frame - @data_frame ||= xpack.data_frame - end + def data_frame + @data_frame ||= xpack.data_frame + end - def ilm - @ilm ||= xpack.ilm - end + def ilm + @ilm ||= xpack.ilm + end - def license - @license ||= xpack.license - end + def license + @license ||= xpack.license + end - def transform - @transform ||= xpack.transform - end + def transform + @transform ||= xpack.transform + end - def async_search - @async_search ||= xpack.async_search - end + def async_search + @async_search ||= xpack.async_search + end - def cat - @cat ||= xpack.cat - end + def cat + @cat ||= xpack.cat + end - def indices - @indices ||= xpack.indices - end + def indices + @indices ||= xpack.indices + end - def searchable_snapshots - @searchable_snapshots ||= xpack.searchable_snapshots - end + def searchable_snapshots + @searchable_snapshots ||= xpack.searchable_snapshots + end - def cross_cluster_replication - @cross_cluster_replication ||= xpack.cross_cluster_replication - end + def cross_cluster_replication + @cross_cluster_replication ||= xpack.cross_cluster_replication + end - def autoscaling - @autoscaling ||= xpack.autoscaling - end + def autoscaling + @autoscaling ||= xpack.autoscaling + end - def enrich - @enrich ||= xpack.enrich - end + def enrich + @enrich ||= xpack.enrich + end - def eql - @eql ||= xpack.eql - end + def eql + @eql ||= xpack.eql + end - def snapshot_lifecycle_management - @snapshot_lifecycle_management ||= xpack.snapshot_lifecycle_management - end + def snapshot_lifecycle_management + @snapshot_lifecycle_management ||= xpack.snapshot_lifecycle_management + end - def text_structure - @text_structure ||= xpack.text_structure - end + def text_structure + @text_structure ||= xpack.text_structure + end - def logstash - @logstash ||= xpack.logstash - end + def logstash + @logstash ||= xpack.logstash + end - def fleet - @fleet ||= xpack.fleet - end + def fleet + @fleet ||= xpack.fleet end end -end if defined?(Elasticsearch::Transport::Client) +end if defined?(Elasticsearch::Client) diff --git a/elasticsearch-xpack/test/test_helper.rb b/elasticsearch-xpack/test/test_helper.rb index 3772e020c9..c452cc462b 100644 --- a/elasticsearch-xpack/test/test_helper.rb +++ b/elasticsearch-xpack/test/test_helper.rb @@ -26,7 +26,7 @@ require 'ansi' -require 'elasticsearch/transport' +require 'elasticsearch' require 'elasticsearch/xpack' Minitest::Reporters.use! Minitest::Reporters::SpecReporter.new @@ -58,7 +58,7 @@ def perform_request(method, path, params, body) end # Top level methods: - Elasticsearch::Transport::Client::TOP_LEVEL_METHODS.each do |method| + Elasticsearch::Client::TOP_LEVEL_METHODS.each do |method| define_method method do |*args| xpack.send(method, *args) end diff --git a/elasticsearch/lib/elasticsearch.rb b/elasticsearch/lib/elasticsearch.rb index 1267f62377..d407394b3e 100644 --- a/elasticsearch/lib/elasticsearch.rb +++ b/elasticsearch/lib/elasticsearch.rb @@ -20,12 +20,102 @@ require 'elasticsearch/api' module Elasticsearch - module Transport - class Client - include Elasticsearch::API + # This is the stateful Elasticsearch::Client, using an instance of elasticsearch-transport. + class Client + include Elasticsearch::API + # The default port to use if connecting using a Cloud ID. + # Updated from 9243 to 443 in client version 7.10.1 + # + # @since 7.2.0 + DEFAULT_CLOUD_PORT = 443 + + # Create a client connected to an Elasticsearch cluster. + # + # @option arguments [String] :cloud_id - The Cloud ID to connect to Elastic Cloud + # @option api_key [String, Hash] :api_key Use API Key Authentication, either the base64 encoding of `id` and `api_key` + # joined by a colon as a String, or a hash with the `id` and `api_key` values. + # @option opaque_id_prefix [String] :opaque_id_prefix set a prefix for X-Opaque-Id when initializing the client. + # This will be prepended to the id you set before each request + # if you're using X-Opaque-Id + def initialize(arguments = {}, &block) + @opaque_id_prefix = arguments[:opaque_id_prefix] || nil + api_key(arguments) if arguments[:api_key] + if arguments[:cloud_id] + arguments[:hosts] = setup_cloud_host( + arguments[:cloud_id], + arguments[:user], + arguments[:password], + arguments[:port] + ) + end + @transport = Elasticsearch::Transport::Client.new(arguments, &block) + end + + def method_missing(name, *args, &block) + if methods.include?(name) + super + elsif name == :perform_request + # The signature for perform_request is: + # method, path, params, body, headers + if (opaque_id = args[2]&.delete(:opaque_id)) + headers = args[4] || {} + opaque_id = @opaque_id_prefix ? "#{@opaque_id_prefix}#{opaque_id}" : opaque_id + args[4] = headers.merge('X-Opaque-Id' => opaque_id) + end + @transport.send(name, *args, &block) + else + @transport.send(name, *args, &block) + end + end + + def respond_to_missing?(method_name, *args) + @transport.respond_to?(method_name) || super + end + + private + + def setup_cloud_host(cloud_id, user, password, port) + name = cloud_id.split(':')[0] + cloud_url, elasticsearch_instance = Base64.decode64(cloud_id.gsub("#{name}:", '')).split('$') + + if cloud_url.include?(':') + url, port = cloud_url.split(':') + host = "#{elasticsearch_instance}.#{url}" + else + host = "#{elasticsearch_instance}.#{cloud_url}" + port ||= DEFAULT_CLOUD_PORT + end + [{ scheme: 'https', user: user, password: password, host: host, port: port.to_i }] + end + + def api_key(arguments) + api_key = if arguments[:api_key].is_a? Hash + encode(arguments[:api_key]) + else + arguments[:api_key] + end + arguments.delete(:user) + arguments.delete(:password) + authorization = { 'Authorization' => "ApiKey #{api_key}" } + if (headers = arguments.dig(:transport_options, :headers)) + headers.merge!(authorization) + else + arguments[:transport_options] = { + headers: authorization + } + end + end + + # Encode credentials for the Authorization Header + # Credentials is the base64 encoding of id and api_key joined by a colon + # @see https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-create-api-key.html + def encode(api_key) + Base64.strict_encode64([api_key[:id], api_key[:api_key]].join(':')) end end end + +# Helper for the meta-header value for Cloud module Elastic # If the version is X.X.X.pre/alpha/beta, use X.X.Xp for the meta-header: def self.client_meta_version diff --git a/elasticsearch/spec/integration/client_integration_spec.rb b/elasticsearch/spec/integration/client_integration_spec.rb index 2a55c1c8f7..d0f07b9471 100644 --- a/elasticsearch/spec/integration/client_integration_spec.rb +++ b/elasticsearch/spec/integration/client_integration_spec.rb @@ -17,9 +17,8 @@ ELASTICSEARCH_URL = ENV['TEST_ES_SERVER'] || "http://localhost:#{(ENV['PORT'] || 9200)}" raise URI::InvalidURIError unless ELASTICSEARCH_URL =~ /\A#{URI::DEFAULT_PARSER.make_regexp}\z/ -require 'elasticsearch' +require 'spec_helper' require 'logger' -require 'rspec' context 'Elasticsearch client' do let(:logger) { Logger.new($stderr) } diff --git a/elasticsearch/spec/spec_helper.rb b/elasticsearch/spec/spec_helper.rb new file mode 100644 index 0000000000..0750a40696 --- /dev/null +++ b/elasticsearch/spec/spec_helper.rb @@ -0,0 +1,31 @@ +# Licensed to Elasticsearch B.V. under one or more contributor +# license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright +# ownership. Elasticsearch B.V. licenses this file to you under +# the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +require 'elasticsearch' +require 'rspec' + +RSpec.configure do |config| + config.formatter = :documentation +end + +def meta_version + client.send(:client_meta_version, Elasticsearch::VERSION) +end + +def jruby? + defined?(JRUBY_VERSION) +end diff --git a/elasticsearch/spec/unit/api_key_spec.rb b/elasticsearch/spec/unit/api_key_spec.rb new file mode 100644 index 0000000000..5df47bd76d --- /dev/null +++ b/elasticsearch/spec/unit/api_key_spec.rb @@ -0,0 +1,101 @@ +# Licensed to Elasticsearch B.V. under one or more contributor +# license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright +# ownership. Elasticsearch B.V. licenses this file to you under +# the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +require 'spec_helper' + +describe Elasticsearch::Client do + context 'when using API Key' do + let(:authorization_header) do + client.transport.connections.first.connection.headers['Authorization'] + end + + context 'when an encoded api_key is provided' do + let(:client) do + described_class.new(api_key: 'an_api_key') + end + + it 'Adds the ApiKey header to the connection' do + expect(authorization_header).to eq('ApiKey an_api_key') + end + end + + context 'when an un-encoded api_key is provided' do + let(:client) do + described_class.new(api_key: { id: 'my_id', api_key: 'my_api_key' }) + end + + it 'Adds the ApiKey header to the connection' do + expect(authorization_header).to eq("ApiKey #{Base64.strict_encode64('my_id:my_api_key')}") + end + end + + context 'when basic auth and api_key are provided' do + let(:client) do + described_class.new( + api_key: { id: 'my_id', api_key: 'my_api_key' }, + host: 'http://elastic:password@localhost:9200' + ) + end + + it 'removes basic auth credentials' do + expect(authorization_header).not_to match(/^Basic/) + expect(authorization_header).to match(/^ApiKey/) + end + end + + context 'when other headers where specified' do + let(:client) do + described_class.new( + api_key: 'elasticsearch_api_key', + transport_options: { headers: { 'x-test-header' => 'test' } } + ) + end + + it 'Adds the ApiKey header to the connection and keeps the header' do + header = client.transport.connections.first.connection.headers + expect(header['Authorization']).to eq('ApiKey elasticsearch_api_key') + expect(header['X-Test-Header']).to eq('test') + end + end + + context 'Metaheader' do + let(:adapter_code) { "nh=#{defined?(Net::HTTP::VERSION) ? Net::HTTP::VERSION : Net::HTTP::HTTPVersion}" } + let(:meta_header) do + if jruby? + "es=#{meta_version},rb=#{RUBY_VERSION},t=#{Elasticsearch::Transport::VERSION},jv=#{ENV_JAVA['java.version']},jr=#{JRUBY_VERSION},fd=#{Faraday::VERSION},#{adapter_code}" + else + "es=#{meta_version},rb=#{RUBY_VERSION},t=#{Elasticsearch::Transport::VERSION},fd=#{Faraday::VERSION},#{adapter_code}" + end + end + + context 'when using API Key' do + let(:client) do + described_class.new(api_key: 'an_api_key') + end + + let(:headers) do + client.transport.connections.first.connection.headers + end + + it 'adds the ApiKey header to the connection' do + expect(authorization_header).to eq('ApiKey an_api_key') + expect(headers).to include('x-elastic-client-meta' => meta_header) + end + end + end + end +end diff --git a/elasticsearch/spec/unit/cloud_credentials_spec.rb b/elasticsearch/spec/unit/cloud_credentials_spec.rb new file mode 100644 index 0000000000..11b411eec8 --- /dev/null +++ b/elasticsearch/spec/unit/cloud_credentials_spec.rb @@ -0,0 +1,167 @@ +# Licensed to Elasticsearch B.V. under one or more contributor +# license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright +# ownership. Elasticsearch B.V. licenses this file to you under +# the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +require 'spec_helper' + +describe Elasticsearch::Client do + context 'when cloud credentials are provided' do + let(:client) do + described_class.new( + cloud_id: 'name:bG9jYWxob3N0JGFiY2QkZWZnaA==', + user: 'elastic', + password: 'changeme' + ) + end + + let(:hosts) do + client.transport.hosts + end + + it 'extracts the cloud credentials' do + expect(hosts[0][:host]).to eq('abcd.localhost') + expect(hosts[0][:protocol]).to eq('https') + expect(hosts[0][:user]).to eq('elastic') + expect(hosts[0][:password]).to eq('changeme') + expect(hosts[0][:port]).to eq(443) + end + + it 'creates the correct full url' do + expect( + client.transport.__full_url(client.transport.hosts[0]) + ).to eq('https://elastic:changeme@abcd.localhost:443') + end + + context 'when a port is specified' do + let(:client) do + described_class.new(cloud_id: 'name:bG9jYWxob3N0JGFiY2QkZWZnaA==', user: 'elastic', password: 'changeme', port: 9250) + end + + it 'sets the specified port along with the cloud credentials' do + expect(hosts[0][:host]).to eq('abcd.localhost') + expect(hosts[0][:protocol]).to eq('https') + expect(hosts[0][:user]).to eq('elastic') + expect(hosts[0][:password]).to eq('changeme') + expect(hosts[0][:port]).to eq(9250) + end + + it 'creates the correct full url' do + expect(client.transport.__full_url(client.transport.hosts[0])).to eq('https://elastic:changeme@abcd.localhost:9250') + end + end + + context 'when the cluster has alternate names' do + let(:client) do + described_class.new( + cloud_id: 'myCluster:bG9jYWxob3N0JGFiY2QkZWZnaA==', + user: 'elasticfantastic', + password: 'tobechanged' + ) + end + + let(:hosts) do + client.transport.hosts + end + + it 'extracts the cloud credentials' do + expect(hosts[0][:host]).to eq('abcd.localhost') + expect(hosts[0][:protocol]).to eq('https') + expect(hosts[0][:user]).to eq('elasticfantastic') + expect(hosts[0][:password]).to eq('tobechanged') + expect(hosts[0][:port]).to eq(443) + end + + it 'creates the correct full url' do + expect( + client.transport.__full_url(client.transport.hosts[0]) + ).to eq('https://elasticfantastic:tobechanged@abcd.localhost:443') + end + end + + context 'when decoded cloud id has a trailing dollar sign' do + let(:client) do + described_class.new( + cloud_id: 'a_cluster:bG9jYWxob3N0JGFiY2Qk', + user: 'elasticfantastic', + password: 'changeme' + ) + end + + let(:hosts) do + client.transport.hosts + end + + it 'extracts the cloud credentials' do + expect(hosts[0][:host]).to eq('abcd.localhost') + expect(hosts[0][:protocol]).to eq('https') + expect(hosts[0][:user]).to eq('elasticfantastic') + expect(hosts[0][:password]).to eq('changeme') + expect(hosts[0][:port]).to eq(443) + end + + it 'creates the correct full url' do + expect( + client.transport.__full_url(client.transport.hosts[0]) + ).to eq('https://elasticfantastic:changeme@abcd.localhost:443') + end + end + + context 'when the cloud host provides a port' do + let(:client) do + described_class.new( + cloud_id: 'name:ZWxhc3RpY19zZXJ2ZXI6OTI0MyRlbGFzdGljX2lk', + user: 'elastic', + password: 'changeme' + ) + end + + let(:hosts) do + client.transport.hosts + end + + it 'creates the correct full url' do + expect(hosts[0][:host]).to eq('elastic_id.elastic_server') + expect(hosts[0][:protocol]).to eq('https') + expect(hosts[0][:user]).to eq('elastic') + expect(hosts[0][:password]).to eq('changeme') + expect(hosts[0][:port]).to eq(9243) + end + end + + context 'when the cloud host provides a port and the port is also specified' do + let(:client) do + described_class.new( + cloud_id: 'name:ZWxhc3RpY19zZXJ2ZXI6OTI0MyRlbGFzdGljX2lk', + user: 'elastic', + password: 'changeme', + port: 9200 + ) + end + + let(:hosts) do + client.transport.hosts + end + + it 'creates the correct full url' do + expect(hosts[0][:host]).to eq('elastic_id.elastic_server') + expect(hosts[0][:protocol]).to eq('https') + expect(hosts[0][:user]).to eq('elastic') + expect(hosts[0][:password]).to eq('changeme') + expect(hosts[0][:port]).to eq(9243) + end + end + end +end diff --git a/elasticsearch/spec/unit/custom_transport_implementation_spec.rb b/elasticsearch/spec/unit/custom_transport_implementation_spec.rb new file mode 100644 index 0000000000..9b004e3f54 --- /dev/null +++ b/elasticsearch/spec/unit/custom_transport_implementation_spec.rb @@ -0,0 +1,43 @@ +# Licensed to Elasticsearch B.V. under one or more contributor +# license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright +# ownership. Elasticsearch B.V. licenses this file to you under +# the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +require 'spec_helper' + +describe Elasticsearch::Client do + context 'when using custom transport implementation' do + class MyTransport + include Elasticsearch::Transport::Transport::Base + def initialize(args); end + end + let(:client) { Elasticsearch::Client.new(transport_class: MyTransport) } + let(:arguments) { client.instance_variable_get('@transport').instance_variable_get('@arguments') } + let(:subject) do + arguments[:transport_options][:headers] + end + + let(:meta_header) do + if jruby? + "es=#{meta_version},rb=#{RUBY_VERSION},t=#{Elasticsearch::Transport::VERSION},jv=#{ENV_JAVA['java.version']},jr=#{JRUBY_VERSION}" + else + "es=#{meta_version},rb=#{RUBY_VERSION},t=#{Elasticsearch::Transport::VERSION}" + end + end + + it 'doesnae set any info about the implementation in the metaheader' do + expect(subject).to include('x-elastic-client-meta' => meta_header) + end + end +end diff --git a/elasticsearch/spec/unit/headers_spec.rb b/elasticsearch/spec/unit/headers_spec.rb new file mode 100644 index 0000000000..75a7ba6375 --- /dev/null +++ b/elasticsearch/spec/unit/headers_spec.rb @@ -0,0 +1,53 @@ +# Licensed to Elasticsearch B.V. under one or more contributor +# license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright +# ownership. Elasticsearch B.V. licenses this file to you under +# the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +require 'spec_helper' +require 'ostruct' + +describe Elasticsearch::Client do + context 'when a header is set on an endpoint request' do + let(:client) { described_class.new } + let(:headers) { { 'user-agent' => 'my ruby app' } } + + it 'performs the request with the header' do + allow(client).to receive(:perform_request) { OpenStruct.new(body: '') } + expect { client.search(headers: headers) }.not_to raise_error + expect(client).to have_received(:perform_request) + .with('GET', '_search', {}, nil, headers) + end + end + + context 'when a header is set on an endpoint request and on initialization' do + let!(:client) do + described_class.new( + host: 'http://localhost:9200', + transport_options: { headers: instance_headers } + ) + end + let(:instance_headers) { { set_in_instantiation: 'header value' } } + let(:param_headers) {{'user-agent' => 'My Ruby Tests', 'set-on-method-call' => 'header value'}} + + it 'performs the request with the header' do + expected_headers = client.transport.connections.connections.first.connection.headers.merge(param_headers) + + expect_any_instance_of(Faraday::Connection) + .to receive(:run_request) + .with(:get, "http://localhost:9200/_search", nil, expected_headers) { OpenStruct.new(body: '')} + + client.search(headers: param_headers) + end + end +end diff --git a/elasticsearch/spec/unit/opaque_id_spec.rb b/elasticsearch/spec/unit/opaque_id_spec.rb new file mode 100644 index 0000000000..40678f70ba --- /dev/null +++ b/elasticsearch/spec/unit/opaque_id_spec.rb @@ -0,0 +1,48 @@ +# Licensed to Elasticsearch B.V. under one or more contributor +# license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright +# ownership. Elasticsearch B.V. licenses this file to you under +# the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +require 'spec_helper' +require 'ostruct' + +describe Elasticsearch::Client do + let(:transport) { client.instance_variable_get('@transport') } + let(:client) { described_class.new } + + before do + allow(transport).to receive(:perform_request) { OpenStruct.new(body: '') } + end + + context 'when x-opaque-id is set' do + it 'uses x-opaque-id on a request' do + client.search(opaque_id: '12345') + expect(transport).to have_received(:perform_request) + .with('GET', '_search', {}, nil, { 'X-Opaque-Id' => '12345' }) + end + end + + context 'when an x-opaque-id prefix is set on initialization' do + let(:prefix) { 'elastic_cloud' } + let(:client) do + described_class.new(opaque_id_prefix: prefix) + end + + it 'uses x-opaque-id on a request' do + expect { client.search(opaque_id: '12345') }.not_to raise_error + expect(transport).to have_received(:perform_request) + .with('GET', '_search', {}, nil, { 'X-Opaque-Id' => 'elastic_cloud12345' }) + end + end +end diff --git a/elasticsearch/spec/unit/wrapper_gem_spec.rb b/elasticsearch/spec/unit/wrapper_gem_spec.rb index 1f37132a56..5ad4382752 100644 --- a/elasticsearch/spec/unit/wrapper_gem_spec.rb +++ b/elasticsearch/spec/unit/wrapper_gem_spec.rb @@ -14,7 +14,8 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. -require 'elasticsearch' + +require 'spec_helper' describe 'Elasticsearch: wrapper gem' do it 'requires all neccessary subgems' do