diff --git a/CHANGELOG.md b/CHANGELOG.md index ed4b727..2032bec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,12 @@ Please mark backwards incompatible changes with an exclamation mark at the start ## [Unreleased] ### Added +- The `#settings` method to the `Elasticsearch::Index` class. This gives the + caller access to the index's settings. +- The `Elasticsearch::Indices::Settings::Blocks` class. The class encapsulates + an index's blocks settings (for example, whether the index is read-only). +- The `Elasticsearch::Indices::Settings` class. The class encapsulates an + index's settings. - It is now possible to configure the type used by the `RSpec::TestDataCollector` class when pushing documents to Elasticsearch. If no type is specified in the configuration the default type will be used. diff --git a/lib/jay_api/elasticsearch.rb b/lib/jay_api/elasticsearch.rb index f4d233e..22588f3 100644 --- a/lib/jay_api/elasticsearch.rb +++ b/lib/jay_api/elasticsearch.rb @@ -7,6 +7,7 @@ require_relative 'elasticsearch/errors' require_relative 'elasticsearch/index' require_relative 'elasticsearch/indexes' +require_relative 'elasticsearch/indices' require_relative 'elasticsearch/query_builder' require_relative 'elasticsearch/query_results' require_relative 'elasticsearch/response' diff --git a/lib/jay_api/elasticsearch/index.rb b/lib/jay_api/elasticsearch/index.rb index 4ca1336..0999228 100644 --- a/lib/jay_api/elasticsearch/index.rb +++ b/lib/jay_api/elasticsearch/index.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require_relative 'indexable' +require_relative 'indices/settings' module JayAPI module Elasticsearch @@ -49,6 +50,13 @@ def index_name def index(data, type: DEFAULT_DOC_TYPE) super.first end + + # @return [JayAPI::Elasticsearch::Indices::Settings] The settings for the + # index. + def settings + # DO NOT MEMOIZE! Leave it to the caller. + ::JayAPI::Elasticsearch::Indices::Settings.new(client.transport_client, index_name) + end end end end diff --git a/lib/jay_api/elasticsearch/indices.rb b/lib/jay_api/elasticsearch/indices.rb new file mode 100644 index 0000000..fcb5cb8 --- /dev/null +++ b/lib/jay_api/elasticsearch/indices.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +require_relative 'indices/settings' + +module JayAPI + module Elasticsearch + # Namespace for sub-elements of Elasticsearch's indices + module Indices; end + end +end diff --git a/lib/jay_api/elasticsearch/indices/settings.rb b/lib/jay_api/elasticsearch/indices/settings.rb new file mode 100644 index 0000000..f30118a --- /dev/null +++ b/lib/jay_api/elasticsearch/indices/settings.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +require_relative 'settings/blocks' + +module JayAPI + module Elasticsearch + module Indices + # Represents the settings of an Elasticsearch Index. + class Settings + attr_reader :transport_client, :index_name + + # @param [Elasticsearch::Transport::Client] transport_client Elasticsearch's + # transport client. + # @param [String] index_name The name of the index this class will be + # handling settings for. + def initialize(transport_client, index_name) + @transport_client = transport_client + @index_name = index_name + end + + # @return [Hash] A Hash with all the settings for the index. It looks + # like this: + # + # { + # "number_of_shards" => "5", + # "blocks" => { "read_only_allow_delete" => "false", "write" => "false" }, + # "provided_name" => "xyz01_tests", + # "creation_date" => "1588701800423", + # "number_of_replicas" => "1", + # "uuid" => "VFx2e5t0Qgi-1zc2PUkYEg", + # "version" => { "created" => "7010199", "upgraded" => "7100299"} + # } + # + # @raise [Elasticsearch::Transport::Transport::Errors::ServerError] If + # an error occurs when trying to get the index's settings. + # @raise [KeyError] If any of the expected hierarchical elements in the + # response are missing. + def all + @all ||= transport_client.indices.get_settings(index: index_name) + .fetch(index_name).fetch('settings').fetch('index') + end + + # @return [JayAPI::Elasticsearch::Indices::Settings::Blocks] The blocks + # settings for the given index. + def blocks + @blocks ||= ::JayAPI::Elasticsearch::Indices::Settings::Blocks.new(self) + end + end + end + end +end diff --git a/lib/jay_api/elasticsearch/indices/settings/blocks.rb b/lib/jay_api/elasticsearch/indices/settings/blocks.rb new file mode 100644 index 0000000..9a7da68 --- /dev/null +++ b/lib/jay_api/elasticsearch/indices/settings/blocks.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +module JayAPI + module Elasticsearch + module Indices + class Settings + # Represents the block settings of an Elasticsearch index. + class Blocks + attr_reader :settings + + # @param [JayAPI::Elasticsearch::Indices::Settings] settings The + # parent +Settings+ object. + def initialize(settings) + @settings = settings + end + + # @return [Boolean] True if the Index has been set to read-only mode, + # false otherwise. + def read_only? + blocks_settings.fetch('write') == 'true' + end + + # Sets the index's +write+ block to the given value. When the +write+ + # block is set to +true+ the index is effectively read-only. + # @param [Boolean] value The new value for the read-only setting of + # the index. + # @raise [Elasticsearch::Transport::Transport::Errors::ServerError] If + # an error occurs when trying to set the value of the property. + def read_only=(value) + unless [true, false].include?(value) + raise ArgumentError, "Expected 'value' to be true or false, #{value.class} given" + end + + return if read_only? == value + + settings.transport_client.indices.put_settings( + index: settings.index_name, + body: { 'blocks.write' => value } + ) + + blocks_settings['write'] = value.to_s + end + + private + + # @return [Hash] The block settings of the index. Something like this: + # + # { 'read_only_allow_delete' => 'false', 'write' => 'false' } + # + # @raise [KeyError] If the index's settings do not contain a "blocks" + # section. + def blocks_settings + @blocks_settings = settings.all.fetch('blocks') + end + end + end + end + end +end diff --git a/spec/jay_api/elasticsearch/index_spec.rb b/spec/jay_api/elasticsearch/index_spec.rb index 8c3dd7e..d39cf52 100644 --- a/spec/jay_api/elasticsearch/index_spec.rb +++ b/spec/jay_api/elasticsearch/index_spec.rb @@ -205,6 +205,32 @@ it_behaves_like 'Indexable#validate_type' end + describe '#settings' do + subject(:method_call) { index.settings } + + let(:settings) do + instance_double( + JayAPI::Elasticsearch::Indices::Settings + ) + end + + before do + allow(JayAPI::Elasticsearch::Indices::Settings) + .to receive(:new).and_return(settings) + end + + it 'creates an instance of the Settings class with the expected parameters' do + expect(JayAPI::Elasticsearch::Indices::Settings) + .to receive(:new).with(transport_client, 'elite_unit_tests') + + method_call + end + + it 'returns the Settings object' do + expect(method_call).to be(settings) + end + end + describe '#queue_size' do subject(:method_call) { index.queue_size } diff --git a/spec/jay_api/elasticsearch/indexable_shared.rb b/spec/jay_api/elasticsearch/indexable_shared.rb index 8384078..4649e27 100644 --- a/spec/jay_api/elasticsearch/indexable_shared.rb +++ b/spec/jay_api/elasticsearch/indexable_shared.rb @@ -3,11 +3,18 @@ RSpec.shared_context 'with mocked objects for Elasticsearch::Indexable' do let(:response) { {} } + let(:transport_client) do + instance_double( + Elasticsearch::Transport::Client + ) + end + let(:client) do instance_double( JayAPI::Elasticsearch::Client, bulk: successful_response, - search: response + search: response, + transport_client: ) end diff --git a/spec/jay_api/elasticsearch/indices/settings/blocks_spec.rb b/spec/jay_api/elasticsearch/indices/settings/blocks_spec.rb new file mode 100644 index 0000000..cb7572b --- /dev/null +++ b/spec/jay_api/elasticsearch/indices/settings/blocks_spec.rb @@ -0,0 +1,238 @@ +# frozen_string_literal: true + +require 'jay_api/elasticsearch/indices/settings' +require 'jay_api/elasticsearch/indices/settings/blocks' + +RSpec.describe JayAPI::Elasticsearch::Indices::Settings::Blocks do + subject(:blocks) { described_class.new(settings) } + + let(:blocks_settings) do + { 'read_only_allow_delete' => 'false', 'write' => 'false' } + end + + let(:index_settings) do + { + 'number_of_shards' => '5', + 'blocks' => blocks_settings, + 'provided_name' => 'xyz01_tests', + 'creation_date' => '1588701800423', + 'number_of_replicas' => '1', + 'uuid' => 'VFx2e5t0Qgi-1zc2PUkYEg', + 'version' => { 'created' => '7010199', 'upgraded' => '7100299' } + } + end + + let(:settings) do + instance_double( + JayAPI::Elasticsearch::Indices::Settings, + all: index_settings, + index_name: 'xyz01_tests' + ) + end + + shared_examples_for '#blocks_settings' do + it 'grabs the settings from the parent Settings object' do + expect(settings).to receive(:all) + method_call + end + + context 'when fetching the settings causes an HTTP error to be raised' do + let(:error) do + [ + Elasticsearch::Transport::Transport::Errors::NotFound, + '404 - Not Found' + ] + end + + before do + allow(settings).to receive(:all).and_raise(*error) + end + + it 're-raises the error' do + expect { method_call }.to raise_error(*error) + end + end + + context 'when fetching the settings causes an KeyError to be raised' do + let(:error) { [KeyError, 'key not found: "xyz01_tests"'] } + + before do + allow(settings).to receive(:all).and_raise(*error) + end + + it 're-raises the error' do + expect { method_call }.to raise_error(*error) + end + end + + context 'when the returned settings do not contain the "blocks" key' do + let(:index_settings) do + super().except('blocks') + end + + it 'raises a KeyError' do + expect { method_call }.to raise_error( + KeyError, 'key not found: "blocks"' + ) + end + end + end + + describe '#read_only?' do + subject(:method_call) { blocks.read_only? } + + it_behaves_like '#blocks_settings' + + context 'when the "write" block is active' do + let(:blocks_settings) { super().merge('write' => 'true') } + + it 'returns true' do + expect(method_call).to be(true) + end + end + + context 'when the "write" block is inactive' do + let(:blocks_settings) { super().merge('write' => 'false') } + + it 'returns false' do + expect(method_call).to be(false) + end + end + + context "when the response doesn't specify the status of the 'write' block" do + let(:blocks_settings) { super().except('write') } + + it 'raises a KeyError' do + expect { method_call }.to raise_error( + KeyError, 'key not found: "write"' + ) + end + end + end + + shared_examples_for '#read_only=' do + it 'takes the transport client from the parent Settings object' do + expect(settings).to receive(:transport_client) + method_call + end + + it "uses the parent Settings object's transport client to set the settings" do + expect(transport_client).to receive(:indices) + expect(indices_client).to receive(:put_settings) + method_call + end + + context "when updating the index's settings raises an HTTP error" do + let(:error) do + [ + Elasticsearch::Transport::Transport::Errors::RequestTimeout, + '408 - Timed out' + ] + end + + before do + allow(indices_client).to receive(:put_settings).and_raise(*error) + end + + it 're-raises the error' do + expect { method_call }.to raise_error(*error) + end + end + + it_behaves_like '#blocks_settings' + end + + describe '#read_only=' do + subject(:method_call) { blocks.read_only = value } + + let(:indices_client) do + instance_double( + Elasticsearch::API::Indices::IndicesClient, + put_settings: true + ) + end + + let(:transport_client) do + instance_double( + Elasticsearch::Transport::Client, + indices: indices_client + ) + end + + before do + allow(settings).to receive(:transport_client).and_return(transport_client) + end + + context "when 'value' is not a Boolean" do + let(:value) { 'true' } + + it 'raises an ArgumentError' do + expect { method_call }.to raise_error( + ArgumentError, "Expected 'value' to be true or false, String given" + ) + end + end + + context 'when the value is set to true' do + let(:value) { true } + + context 'when the index is already read-only' do + let(:blocks_settings) { super().merge('write' => 'true') } + + it "does not try to set the index's settings" do + expect(indices_client).not_to receive(:put_settings) + method_call + end + end + + context 'when the index is not already read-only' do + let(:blocks_settings) { super().merge('write' => 'false') } + + it_behaves_like '#read_only=' + + it 'sets the expected index settings' do + expect(indices_client).to receive(:put_settings).with( + index: 'xyz01_tests', body: { 'blocks.write' => true } + ) + + method_call + end + + it 'changes the value of read_only?' do + expect { method_call }.to change(blocks, :read_only?).from(false).to(true) + end + end + end + + context 'when the value is set to false' do + let(:value) { false } + + context 'when the index is read-only' do + let(:blocks_settings) { super().merge('write' => 'true') } + + it_behaves_like '#read_only=' + + it 'sets the expected index settings' do + expect(indices_client).to receive(:put_settings).with( + index: 'xyz01_tests', body: { 'blocks.write' => false } + ) + + method_call + end + + it 'changes the value of read_only?' do + expect { method_call }.to change(blocks, :read_only?).from(true).to(false) + end + end + + context 'when the index is not read-only' do + let(:blocks_settings) { super().merge('write' => 'false') } + + it "does not try to set the index's settings" do + expect(indices_client).not_to receive(:put_settings) + method_call + end + end + end + end +end diff --git a/spec/jay_api/elasticsearch/indices/settings_spec.rb b/spec/jay_api/elasticsearch/indices/settings_spec.rb new file mode 100644 index 0000000..bb15c06 --- /dev/null +++ b/spec/jay_api/elasticsearch/indices/settings_spec.rb @@ -0,0 +1,163 @@ +# frozen_string_literal: true + +require 'jay_api/elasticsearch/indices/settings' + +RSpec.describe JayAPI::Elasticsearch::Indices::Settings do + subject(:settings) { described_class.new(transport_client, index_name) } + + let(:indices_client) do + instance_double( + Elasticsearch::API::Indices::IndicesClient + ) + end + + let(:transport_client) do + instance_double( + Elasticsearch::Transport::Client, + indices: indices_client + ) + end + + let(:index_name) { 'xyz01_tests' } + + describe '#all' do + subject(:method_call) { settings.all } + + let(:index_settings) do + { + 'xyz01_tests' => { + 'settings' => { + 'index' => { + 'number_of_shards' => '5', + 'blocks' => { 'read_only_allow_delete' => 'false', 'write' => 'false' }, + 'provided_name' => 'xyz01_tests', + 'creation_date' => '1588701800423', + 'number_of_replicas' => '1', + 'uuid' => 'VFx2e5t0Qgi-1zc2PUkYEg', + 'version' => { 'created' => '7010199', 'upgraded' => '7100299' } + } + } + } + } + end + + let(:expected_hash) do + { + 'number_of_shards' => '5', + 'blocks' => { 'read_only_allow_delete' => 'false', 'write' => 'false' }, + 'provided_name' => 'xyz01_tests', + 'creation_date' => '1588701800423', + 'number_of_replicas' => '1', + 'uuid' => 'VFx2e5t0Qgi-1zc2PUkYEg', + 'version' => { 'created' => '7010199', 'upgraded' => '7100299' } + } + end + + before do + allow(indices_client).to receive(:get_settings) + .with(index: index_name).and_return(index_settings) + end + + it "fetches the index's settings using the transport client" do + expect(transport_client).to receive(:indices) + expect(indices_client).to receive(:get_settings).with(index: index_name) + method_call + end + + it 'returns the expected hash' do + expect(method_call).to eq(expected_hash) + end + + context 'when an HTTP error occurs' do + let(:error) do + [ + Elasticsearch::Transport::Transport::Errors::Unauthorized, + '401 - Unauthorized' + ] + end + + before do + allow(indices_client).to receive(:get_settings).and_raise(*error) + end + + it 're-raises the error' do + expect { method_call }.to raise_error(*error) + end + end + + context "when the response doesn't contain the setting for the expected index" do + let(:index_settings) do + { + 'xyz01_requirements' => { # <== Different index + 'settings' => { + 'index' => { + 'routing' => { 'allocation' => { 'initial_recovery' => { '_id' => nil } } }, + 'number_of_shards' => '5', + 'routing_partition_size' => '1', + 'blocks' => { 'read_only_allow_delete' => 'false', 'write' => 'false' }, + 'provided_name' => 'xyz01_requirements', + 'creation_date' => '1770731215226', + 'number_of_replicas' => '1', + 'uuid' => 'M00JT4urRsSPRytAWObGCA', + 'version' => { 'created' => '135248027', 'upgraded' => '135248027' } + } + } + } + } + end + + it 'raises a KeyError' do + expect { method_call }.to raise_error(KeyError, 'key not found: "xyz01_tests"') + end + end + + context "when the response doesn't contain the settings" do + let(:index_settings) do + { + 'xyz01_tests' => {} + } + end + + it 'raises a KeyError' do + expect { method_call }.to raise_error(KeyError, 'key not found: "settings"') + end + end + + context "when the response doesn't contain the index's settings" do + let(:index_settings) do + { + 'xyz01_tests' => { + 'settings' => {} + } + } + end + + it 'raises a KeyError' do + expect { method_call }.to raise_error(KeyError, 'key not found: "index"') + end + end + end + + describe '#blocks' do + subject(:method_call) { settings.blocks } + + let(:blocks) do + instance_double( + JayAPI::Elasticsearch::Indices::Settings::Blocks + ) + end + + before do + allow(JayAPI::Elasticsearch::Indices::Settings::Blocks).to receive(:new).and_return(blocks) + end + + it 'creates an instance of the Blocks class with the expected parameters' do + expect(JayAPI::Elasticsearch::Indices::Settings::Blocks).to receive(:new).with(settings) + method_call + end + + it 'returns the instance of the Blocks class' do + expect(method_call).to be(blocks) + end + end +end