elasticgraph-admin 0.18.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +3 -0
- data/elasticgraph-admin.gemspec +23 -0
- data/lib/elastic_graph/admin/cluster_configurator/action_reporter.rb +23 -0
- data/lib/elastic_graph/admin/cluster_configurator/cluster_settings_manager.rb +99 -0
- data/lib/elastic_graph/admin/cluster_configurator/script_configurator.rb +54 -0
- data/lib/elastic_graph/admin/cluster_configurator.rb +104 -0
- data/lib/elastic_graph/admin/datastore_client_dry_run_decorator.rb +76 -0
- data/lib/elastic_graph/admin/index_definition_configurator/for_index.rb +194 -0
- data/lib/elastic_graph/admin/index_definition_configurator/for_index_template.rb +247 -0
- data/lib/elastic_graph/admin/index_definition_configurator.rb +24 -0
- data/lib/elastic_graph/admin/rake_tasks.rb +129 -0
- data/lib/elastic_graph/admin.rb +97 -0
- metadata +432 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 6202cf82573c7c499ad9615f590f14272dda1b605617f5680bcec1f97a648be6
|
4
|
+
data.tar.gz: eb0f47afd2cafb1a91b868097ac6e65f51ba4bef76ff307762c6f10bc2a1fbaa
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 641c7cf2668a7ae77b4aa7e53d333193e09e052c834cff99792dd75a2213a9b6c441c021665ebf6cc9a5b425368ff8ecec8e1af48bc8cc7d8b7dfdfa8d8a13ff
|
7
|
+
data.tar.gz: ed4aede0dfa748e7a6f97bf71d51112e6b5e1d97c44a8fd2a08f928063c23b878c4bcae1d8ae4872094229e304328422250885ae43e92fdb054264ee6925ac69
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2024 Block, Inc.
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# Copyright 2024 Block, Inc.
|
2
|
+
#
|
3
|
+
# Use of this source code is governed by an MIT-style
|
4
|
+
# license that can be found in the LICENSE file or at
|
5
|
+
# https://opensource.org/licenses/MIT.
|
6
|
+
#
|
7
|
+
# frozen_string_literal: true
|
8
|
+
|
9
|
+
require_relative "../gemspec_helper"
|
10
|
+
|
11
|
+
ElasticGraphGemspecHelper.define_elasticgraph_gem(gemspec_file: __FILE__, category: :core) do |spec, eg_version|
|
12
|
+
spec.summary = "ElasticGraph gem that provides datastore administrative tasks, to keep a datastore up-to-date with an ElasticGraph schema."
|
13
|
+
|
14
|
+
spec.add_dependency "elasticgraph-datastore_core", eg_version
|
15
|
+
spec.add_dependency "elasticgraph-indexer", eg_version
|
16
|
+
spec.add_dependency "elasticgraph-schema_artifacts", eg_version
|
17
|
+
spec.add_dependency "elasticgraph-support", eg_version
|
18
|
+
spec.add_dependency "rake", "~> 13.2"
|
19
|
+
|
20
|
+
spec.add_development_dependency "elasticgraph-elasticsearch", eg_version
|
21
|
+
spec.add_development_dependency "elasticgraph-opensearch", eg_version
|
22
|
+
spec.add_development_dependency "elasticgraph-schema_definition", eg_version
|
23
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# Copyright 2024 Block, Inc.
|
2
|
+
#
|
3
|
+
# Use of this source code is governed by an MIT-style
|
4
|
+
# license that can be found in the LICENSE file or at
|
5
|
+
# https://opensource.org/licenses/MIT.
|
6
|
+
#
|
7
|
+
# frozen_string_literal: true
|
8
|
+
|
9
|
+
module ElasticGraph
|
10
|
+
class Admin
|
11
|
+
class ClusterConfigurator
|
12
|
+
class ActionReporter
|
13
|
+
def initialize(output)
|
14
|
+
@output = output
|
15
|
+
end
|
16
|
+
|
17
|
+
def report_action(message)
|
18
|
+
@output.puts "#{message.chomp}\n#{"=" * 80}\n"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
# Copyright 2024 Block, Inc.
|
2
|
+
#
|
3
|
+
# Use of this source code is governed by an MIT-style
|
4
|
+
# license that can be found in the LICENSE file or at
|
5
|
+
# https://opensource.org/licenses/MIT.
|
6
|
+
#
|
7
|
+
# frozen_string_literal: true
|
8
|
+
|
9
|
+
require "elastic_graph/error"
|
10
|
+
|
11
|
+
module ElasticGraph
|
12
|
+
class Admin
|
13
|
+
class ClusterConfigurator
|
14
|
+
# Responsible for updating datastore cluster settings based on the mode EG is in, maintenance mode or indexing mode
|
15
|
+
class ClusterSettingsManager
|
16
|
+
def initialize(datastore_clients_by_name:, datastore_config:, logger:)
|
17
|
+
@datastore_clients_by_name = datastore_clients_by_name
|
18
|
+
@datastore_config = datastore_config
|
19
|
+
@logger = logger
|
20
|
+
end
|
21
|
+
|
22
|
+
# Starts index maintenance mode, if it has not already been started. This method is idempotent.
|
23
|
+
#
|
24
|
+
# In index maintenance mode, you can safely delete or update the index configuration without
|
25
|
+
# worrying about indices being auto-created with dynamic mappings (e.g. due to an indexing
|
26
|
+
# race condition). While in this mode, indexing operations on documents that fall into new rollover
|
27
|
+
# indices may fail since the auto-creation of those indices is disabled.
|
28
|
+
#
|
29
|
+
# `cluster_spec` can be the name of a specific cluster (as a string) or `:all_clusters`.
|
30
|
+
def start_index_maintenance_mode!(cluster_spec)
|
31
|
+
cluster_names_for(cluster_spec).each do |cluster_name|
|
32
|
+
datastore_client_named(cluster_name).put_persistent_cluster_settings(desired_cluster_settings(cluster_name))
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Ends index maintenance mode, if it has not already ended. This method is idempotent.
|
37
|
+
#
|
38
|
+
# Outside of this mode, you cannot safely delete or update the index configuration. However,
|
39
|
+
# new rollover indices will correctly be auto-created as documents that fall in new months or
|
40
|
+
# years are indexed.
|
41
|
+
#
|
42
|
+
# `cluster_spec` can be the name of a specific cluster (as a string) or `:all_clusters`.
|
43
|
+
def end_index_maintenance_mode!(cluster_spec)
|
44
|
+
cluster_names_for(cluster_spec).each do |cluster_name|
|
45
|
+
datastore_client_named(cluster_name).put_persistent_cluster_settings(
|
46
|
+
desired_cluster_settings(cluster_name, auto_create_index_patterns: ["*#{ROLLOVER_INDEX_INFIX_MARKER}*"])
|
47
|
+
)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Runs a block in index maintenance mode. Should be used to wrap any code that updates your index configuration.
|
52
|
+
#
|
53
|
+
# `cluster_spec` can be the name of a specific cluster (as a string) or `:all_clusters`.
|
54
|
+
def in_index_maintenance_mode(cluster_spec)
|
55
|
+
start_index_maintenance_mode!(cluster_spec)
|
56
|
+
|
57
|
+
begin
|
58
|
+
yield
|
59
|
+
rescue => e
|
60
|
+
@logger.warn "WARNING: ClusterSettingsManager#in_index_maintenance_mode is not able to exit index maintenance mode due to exception #{e}.\n A bit of manual cleanup may be required (although a re-try should be idempotent)."
|
61
|
+
raise # re-raise the same error
|
62
|
+
else
|
63
|
+
# Note: we intentionally do not end maintenance mode in an `ensure` block, because if an exception
|
64
|
+
# happens while we `yield`, we do _not_ want to exit maintenance mode. Exiting maintenance mode
|
65
|
+
# could put us in a state where indices are dynamically created when we do not want them to be.
|
66
|
+
end_index_maintenance_mode!(cluster_spec)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def desired_cluster_settings(cluster_name, auto_create_index_patterns: [])
|
73
|
+
{
|
74
|
+
# https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-index_.html#index-creation
|
75
|
+
#
|
76
|
+
# We generally want to disable automatic index creation in order to require all indices to be properly
|
77
|
+
# defined and configured. However, we must allow kibana to create some indices for it to be usable
|
78
|
+
# (https://discuss.elastic.co/t/elasticsearchs-action-auto-create-index-setting-impact-on-kibana/117701).
|
79
|
+
"action.auto_create_index" => ([".kibana*"] + auto_create_index_patterns).map { |p| "+#{p}" }.join(",")
|
80
|
+
}.merge(@datastore_config.clusters.fetch(cluster_name).settings)
|
81
|
+
end
|
82
|
+
|
83
|
+
def datastore_client_named(cluster_name)
|
84
|
+
@datastore_clients_by_name.fetch(cluster_name) do
|
85
|
+
raise ClusterOperationError,
|
86
|
+
"Unknown datastore cluster name: `#{cluster_name}`. Valid cluster names: #{@datastore_clients_by_name.keys}"
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def cluster_names_for(cluster_spec)
|
91
|
+
case cluster_spec
|
92
|
+
when :all_clusters then @datastore_clients_by_name.keys
|
93
|
+
else [cluster_spec]
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# Copyright 2024 Block, Inc.
|
2
|
+
#
|
3
|
+
# Use of this source code is governed by an MIT-style
|
4
|
+
# license that can be found in the LICENSE file or at
|
5
|
+
# https://opensource.org/licenses/MIT.
|
6
|
+
#
|
7
|
+
# frozen_string_literal: true
|
8
|
+
|
9
|
+
require "elastic_graph/admin/cluster_configurator/action_reporter"
|
10
|
+
require "elastic_graph/error"
|
11
|
+
|
12
|
+
module ElasticGraph
|
13
|
+
class Admin
|
14
|
+
class ClusterConfigurator
|
15
|
+
class ScriptConfigurator
|
16
|
+
def initialize(datastore_client:, script_context:, script_id:, script:, output:)
|
17
|
+
@datastore_client = datastore_client
|
18
|
+
@script_context = script_context
|
19
|
+
@script_id = script_id
|
20
|
+
@script = script
|
21
|
+
@action_reporter = ActionReporter.new(output)
|
22
|
+
end
|
23
|
+
|
24
|
+
def validate
|
25
|
+
case existing_datastore_script
|
26
|
+
when :not_found, @script
|
27
|
+
[]
|
28
|
+
else
|
29
|
+
[
|
30
|
+
"#{@script_context} script #{@script_id} already exists in the datastore but has different contents. " \
|
31
|
+
"\n\nScript in the datastore:\n#{::YAML.dump(existing_datastore_script)}" \
|
32
|
+
"\n\nDesired script:\n#{::YAML.dump(@script)}"
|
33
|
+
]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def configure!
|
38
|
+
if existing_datastore_script == :not_found
|
39
|
+
@datastore_client.put_script(id: @script_id, body: {script: @script}, context: @script_context)
|
40
|
+
@action_reporter.report_action "Stored #{@script_context} script: #{@script_id}"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def existing_datastore_script
|
47
|
+
@existing_datastore_script ||= @datastore_client
|
48
|
+
.get_script(id: @script_id)
|
49
|
+
&.fetch("script") || :not_found
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
# Copyright 2024 Block, Inc.
|
2
|
+
#
|
3
|
+
# Use of this source code is governed by an MIT-style
|
4
|
+
# license that can be found in the LICENSE file or at
|
5
|
+
# https://opensource.org/licenses/MIT.
|
6
|
+
#
|
7
|
+
# frozen_string_literal: true
|
8
|
+
|
9
|
+
require "elastic_graph/admin/cluster_configurator/script_configurator"
|
10
|
+
require "elastic_graph/admin/index_definition_configurator"
|
11
|
+
require "elastic_graph/error"
|
12
|
+
require "stringio"
|
13
|
+
|
14
|
+
module ElasticGraph
|
15
|
+
class Admin
|
16
|
+
# Facade responsible for overall cluster configuration. Delegates to other classes as
|
17
|
+
# necessary to configure different aspects of the cluster (such as index configuration,
|
18
|
+
# cluster settings, etc).
|
19
|
+
class ClusterConfigurator
|
20
|
+
def initialize(
|
21
|
+
datastore_clients_by_name:,
|
22
|
+
index_defs:,
|
23
|
+
index_configurations_by_name:,
|
24
|
+
index_template_configurations_by_name:,
|
25
|
+
scripts:,
|
26
|
+
cluster_settings_manager:,
|
27
|
+
clock:
|
28
|
+
)
|
29
|
+
@datastore_clients_by_name = datastore_clients_by_name
|
30
|
+
@index_defs = index_defs
|
31
|
+
@index_configurations_by_name = index_configurations_by_name.merge(index_template_configurations_by_name)
|
32
|
+
@scripts_by_id = scripts
|
33
|
+
@cluster_settings_manager = cluster_settings_manager
|
34
|
+
@clock = clock
|
35
|
+
end
|
36
|
+
|
37
|
+
# Attempts to configure all aspects of the datastore cluster. Known/expected failure
|
38
|
+
# cases are pre-validated so that an error can be raised before applying any changes to
|
39
|
+
# any indices, so that we hopefully don't wind up in a "partially configured" state.
|
40
|
+
def configure_cluster(output)
|
41
|
+
# Note: we do not want to cache `index_configurators_for` here in a variable, because it's important
|
42
|
+
# for our tests that different instances are used for `validate` vs `configure!`. That's the case because
|
43
|
+
# each `index_configurator` memoizes some datastore responses (e.g. when it fetches the settings or
|
44
|
+
# mappings for an index...). In our tests, we use different datastore clients that connect to the same
|
45
|
+
# datastore server, and that means that when we reuse the same `index_configurator`, the datastore
|
46
|
+
# index winds up being mutated (via another client) in between `validate` and `configure!` breaking assumptions
|
47
|
+
# of the datastore response memoization. By using different index configurators for the two steps it
|
48
|
+
# avoids some odd bugs.
|
49
|
+
script_configurators = script_configurators_for(output)
|
50
|
+
|
51
|
+
errors = script_configurators.flat_map(&:validate) + index_definition_configurators_for(output).flat_map(&:validate)
|
52
|
+
|
53
|
+
if errors.any?
|
54
|
+
error_descriptions = errors.map.with_index do |error, index|
|
55
|
+
"#{index + 1}): #{error}"
|
56
|
+
end.join("\n#{"=" * 80}\n\n")
|
57
|
+
|
58
|
+
raise ClusterOperationError, "Got #{errors.size} validation error(s):\n\n#{error_descriptions}"
|
59
|
+
end
|
60
|
+
|
61
|
+
script_configurators.each(&:configure!)
|
62
|
+
|
63
|
+
@cluster_settings_manager.in_index_maintenance_mode(:all_clusters) do
|
64
|
+
index_definition_configurators_for(output).each(&:configure!)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def accessible_index_definitions
|
69
|
+
@accessible_index_definitions ||= @index_defs.reject { |i| i.all_accessible_cluster_names.empty? }
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def script_configurators_for(output)
|
75
|
+
# It's a bit tricky to know which datastore cluster a script is needed in (the script metadata
|
76
|
+
# doesn't store that), but storing a script in a cluster that doesn't need it causes no harm. The
|
77
|
+
# id of each script contains the hash of its contents so there's no possibility of different clusters
|
78
|
+
# needing a script with the same `id` to have different contents. So here we create a script configurator
|
79
|
+
# for each datastore client.
|
80
|
+
@datastore_clients_by_name.values.flat_map do |datastore_client|
|
81
|
+
@scripts_by_id.map do |id, payload|
|
82
|
+
ScriptConfigurator.new(
|
83
|
+
datastore_client: datastore_client,
|
84
|
+
script_context: payload.fetch("context"),
|
85
|
+
script_id: id,
|
86
|
+
script: payload.fetch("script"),
|
87
|
+
output: output
|
88
|
+
)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def index_definition_configurators_for(output)
|
94
|
+
@index_defs.flat_map do |index_def|
|
95
|
+
env_agnostic_config = @index_configurations_by_name.fetch(index_def.name)
|
96
|
+
|
97
|
+
index_def.all_accessible_cluster_names.map do |cluster_name|
|
98
|
+
IndexDefinitionConfigurator.new(@datastore_clients_by_name.fetch(cluster_name), index_def, env_agnostic_config, output, @clock)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# Copyright 2024 Block, Inc.
|
2
|
+
#
|
3
|
+
# Use of this source code is governed by an MIT-style
|
4
|
+
# license that can be found in the LICENSE file or at
|
5
|
+
# https://opensource.org/licenses/MIT.
|
6
|
+
#
|
7
|
+
# frozen_string_literal: true
|
8
|
+
|
9
|
+
require "forwardable"
|
10
|
+
|
11
|
+
module ElasticGraph
|
12
|
+
class Admin
|
13
|
+
# Decorator that wraps a datastore client in order to implement dry run behavior.
|
14
|
+
# All write operations are implemented as no-ops, while read operations are passed through
|
15
|
+
# to the wrapped datastore client.
|
16
|
+
#
|
17
|
+
# We prefer this over having to check a `dry_run` flag in many places because that's
|
18
|
+
# easy to forget. One mistake and a dry run isn't truly a dry run!
|
19
|
+
#
|
20
|
+
# In contrast, this gives us a strong guarantee that dry run mode truly avoids mutating
|
21
|
+
# any datastore state. This decorator specifically picks and chooses which operations it
|
22
|
+
# allows.
|
23
|
+
#
|
24
|
+
# - Read operations are forwarded to the wrapped datastore client.
|
25
|
+
# - Write operations are implemented as no-ops.
|
26
|
+
#
|
27
|
+
# If/when the calling code evolves to call a new method on this, it'll trigger
|
28
|
+
# `NoMethodError`, giving us a good chance to evaluate how this decorator should
|
29
|
+
# support a particular API. This is also why this doesn't use Ruby's `delegate` library,
|
30
|
+
# because we don't want methods automatically delegated; we want to opt-in to only the read-only methods.
|
31
|
+
class DatastoreClientDryRunDecorator
|
32
|
+
extend Forwardable
|
33
|
+
|
34
|
+
def initialize(wrapped_client)
|
35
|
+
@wrapped_client = wrapped_client
|
36
|
+
end
|
37
|
+
|
38
|
+
# Cluster APIs
|
39
|
+
def_delegators :@wrapped_client, :get_flat_cluster_settings, :get_cluster_health
|
40
|
+
|
41
|
+
def put_persistent_cluster_settings(*) = nil
|
42
|
+
|
43
|
+
# Script APIs
|
44
|
+
def_delegators :@wrapped_client, :get_script
|
45
|
+
|
46
|
+
def put_script(*) = nil
|
47
|
+
|
48
|
+
def delete_script(*) = nil
|
49
|
+
|
50
|
+
# Index Template APIs
|
51
|
+
def_delegators :@wrapped_client, :get_index_template
|
52
|
+
|
53
|
+
def delete_index_template(*) = nil
|
54
|
+
|
55
|
+
def put_index_template(*) = nil
|
56
|
+
|
57
|
+
# Index APIs
|
58
|
+
def_delegators :@wrapped_client, :get_index, :list_indices_matching
|
59
|
+
|
60
|
+
def delete_indices(*) = nil
|
61
|
+
|
62
|
+
def create_index(*) = nil
|
63
|
+
|
64
|
+
def put_index_mapping(*) = nil
|
65
|
+
|
66
|
+
def put_index_settings(*) = nil
|
67
|
+
|
68
|
+
# Document APIs
|
69
|
+
def_delegators :@wrapped_client, :get, :search, :msearch
|
70
|
+
|
71
|
+
def delete_all_documents(*) = nil
|
72
|
+
|
73
|
+
def bulk(*) = nil
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,194 @@
|
|
1
|
+
# Copyright 2024 Block, Inc.
|
2
|
+
#
|
3
|
+
# Use of this source code is governed by an MIT-style
|
4
|
+
# license that can be found in the LICENSE file or at
|
5
|
+
# https://opensource.org/licenses/MIT.
|
6
|
+
#
|
7
|
+
# frozen_string_literal: true
|
8
|
+
|
9
|
+
require "elastic_graph/admin/cluster_configurator/action_reporter"
|
10
|
+
require "elastic_graph/datastore_core/index_config_normalizer"
|
11
|
+
require "elastic_graph/indexer/hash_differ"
|
12
|
+
require "elastic_graph/support/hash_util"
|
13
|
+
|
14
|
+
module ElasticGraph
|
15
|
+
class Admin
|
16
|
+
module IndexDefinitionConfigurator
|
17
|
+
# Responsible for managing an index's configuration, including both mappings and settings.
|
18
|
+
class ForIndex
|
19
|
+
# @dynamic index
|
20
|
+
|
21
|
+
attr_reader :index
|
22
|
+
|
23
|
+
def initialize(datastore_client, index, env_agnostic_index_config, output)
|
24
|
+
@datastore_client = datastore_client
|
25
|
+
@index = index
|
26
|
+
@env_agnostic_index_config = env_agnostic_index_config
|
27
|
+
@reporter = ClusterConfigurator::ActionReporter.new(output)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Attempts to idempotently update the index configuration to the desired configuration
|
31
|
+
# exposed by the `IndexDefinition` object. Based on the configuration of the passed index
|
32
|
+
# and the state of the index in the datastore, does one of the following:
|
33
|
+
#
|
34
|
+
# - If the index did not already exist: creates the index with the desired mappings and settings.
|
35
|
+
# - If the desired mapping has fewer fields than what is in the index: raises an exception,
|
36
|
+
# because the datastore provides no way to remove fields from a mapping and it would be confusing
|
37
|
+
# for this method to silently ignore the issue.
|
38
|
+
# - If the settings have desired changes: updates the settings, restoring any setting that
|
39
|
+
# no longer has a desired value to its default.
|
40
|
+
# - If the mapping has desired changes: updates the mappings.
|
41
|
+
#
|
42
|
+
# Note that any of the writes to the index may fail. There are many things that cannot
|
43
|
+
# be changed on an existing index (such as static settings, field mapping types, etc). We do not attempt
|
44
|
+
# to validate those things ahead of time and instead rely on the datastore to fail if an invalid operation
|
45
|
+
# is attempted.
|
46
|
+
def configure!
|
47
|
+
return create_new_index unless index_exists?
|
48
|
+
|
49
|
+
# Update settings before mappings, to front-load the API call that is more likely to fail.
|
50
|
+
# Our `validate` method guards against mapping changes that are known to be disallowed by
|
51
|
+
# the datastore, but it is much harder to validate that for settings, because there are so
|
52
|
+
# many settings, and there is not clear documentation that outlines all settings, which can
|
53
|
+
# be updated on existing indices, etc.
|
54
|
+
#
|
55
|
+
# If we get a failure, we'd rather it happen before any changes are applied to the index, instead
|
56
|
+
# of applying the mappings and then failing on the settings.
|
57
|
+
update_settings if settings_updates.any?
|
58
|
+
|
59
|
+
update_mapping if has_mapping_updates?
|
60
|
+
end
|
61
|
+
|
62
|
+
def validate
|
63
|
+
if index_exists? && mapping_type_changes.any?
|
64
|
+
[cannot_modify_mapping_field_type_error]
|
65
|
+
else
|
66
|
+
[]
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def create_new_index
|
73
|
+
@datastore_client.create_index(index: @index.name, body: desired_config)
|
74
|
+
report_action "Created index: `#{@index.name}`"
|
75
|
+
end
|
76
|
+
|
77
|
+
def update_mapping
|
78
|
+
@datastore_client.put_index_mapping(index: @index.name, body: desired_mapping)
|
79
|
+
action_description = "Updated mappings for index `#{@index.name}`:\n#{mapping_diff}"
|
80
|
+
|
81
|
+
if mapping_removals.any?
|
82
|
+
action_description += "\n\nNote: the extra fields listed here will not actually get removed. " \
|
83
|
+
"Mapping removals are unsupported (but ElasticGraph will leave them alone and they'll cause no problems)."
|
84
|
+
end
|
85
|
+
|
86
|
+
report_action action_description
|
87
|
+
end
|
88
|
+
|
89
|
+
def update_settings
|
90
|
+
@datastore_client.put_index_settings(index: @index.name, body: settings_updates)
|
91
|
+
report_action "Updated settings for index `#{@index.name}`:\n#{settings_diff}"
|
92
|
+
end
|
93
|
+
|
94
|
+
def cannot_modify_mapping_field_type_error
|
95
|
+
"The datastore does not support modifying the type of a field from an existing index definition. " \
|
96
|
+
"You are attempting to update type of fields (#{mapping_type_changes.inspect}) from the #{@index.name} index definition."
|
97
|
+
end
|
98
|
+
|
99
|
+
def index_exists?
|
100
|
+
!current_config.empty?
|
101
|
+
end
|
102
|
+
|
103
|
+
def mapping_removals
|
104
|
+
@mapping_removals ||= mapping_fields_from(current_mapping) - mapping_fields_from(desired_mapping)
|
105
|
+
end
|
106
|
+
|
107
|
+
def mapping_type_changes
|
108
|
+
@mapping_type_changes ||= begin
|
109
|
+
flattened_current = Support::HashUtil.flatten_and_stringify_keys(current_mapping)
|
110
|
+
flattened_desired = Support::HashUtil.flatten_and_stringify_keys(desired_mapping)
|
111
|
+
|
112
|
+
flattened_current.keys.select do |key|
|
113
|
+
key.end_with?(".type") && flattened_desired.key?(key) && flattened_desired[key] != flattened_current[key]
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def has_mapping_updates?
|
119
|
+
current_mapping != desired_mapping
|
120
|
+
end
|
121
|
+
|
122
|
+
def settings_updates
|
123
|
+
@settings_updates ||= begin
|
124
|
+
# Updating a setting to null will cause the datastore to restore the default value of the setting.
|
125
|
+
restore_to_defaults = (current_settings.keys - desired_settings.keys).to_h { |key| [key, nil] }
|
126
|
+
desired_settings.select { |key, value| current_settings[key] != value }.merge(restore_to_defaults)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def mapping_fields_from(mapping_hash, prefix = "")
|
131
|
+
(mapping_hash["properties"] || []).flat_map do |key, params|
|
132
|
+
field = prefix + key
|
133
|
+
if params.key?("properties")
|
134
|
+
[field] + mapping_fields_from(params, "#{field}.")
|
135
|
+
else
|
136
|
+
[field]
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def desired_mapping
|
142
|
+
desired_config.fetch("mappings")
|
143
|
+
end
|
144
|
+
|
145
|
+
def desired_settings
|
146
|
+
@desired_settings ||= desired_config.fetch("settings")
|
147
|
+
end
|
148
|
+
|
149
|
+
def desired_config
|
150
|
+
@desired_config ||= begin
|
151
|
+
# _meta is place where we can record state on the index mapping in the datastore.
|
152
|
+
# We want to maintain `_meta.ElasticGraph.sources` as an append-only set of all sources that have ever
|
153
|
+
# been configured to flow into an index, so that we can remember whether or not an index which currently
|
154
|
+
# has no `sourced_from` from fields ever did. This is necessary for our automatic filtering of multi-source
|
155
|
+
# indexes.
|
156
|
+
previously_recorded_sources = current_mapping.dig("_meta", "ElasticGraph", "sources") || []
|
157
|
+
sources = previously_recorded_sources.union(@index.current_sources.to_a).sort
|
158
|
+
|
159
|
+
DatastoreCore::IndexConfigNormalizer.normalize(Support::HashUtil.deep_merge(@env_agnostic_index_config, {
|
160
|
+
"mappings" => {"_meta" => {"ElasticGraph" => {"sources" => sources}}},
|
161
|
+
"settings" => @index.flattened_env_setting_overrides
|
162
|
+
}))
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
def current_mapping
|
167
|
+
current_config["mappings"] || {}
|
168
|
+
end
|
169
|
+
|
170
|
+
def current_settings
|
171
|
+
@current_settings ||= current_config["settings"]
|
172
|
+
end
|
173
|
+
|
174
|
+
def current_config
|
175
|
+
@current_config ||= DatastoreCore::IndexConfigNormalizer.normalize(
|
176
|
+
@datastore_client.get_index(@index.name)
|
177
|
+
)
|
178
|
+
end
|
179
|
+
|
180
|
+
def mapping_diff
|
181
|
+
@mapping_diff ||= Indexer::HashDiffer.diff(current_mapping, desired_mapping) || "(no diff)"
|
182
|
+
end
|
183
|
+
|
184
|
+
def settings_diff
|
185
|
+
@settings_diff ||= Indexer::HashDiffer.diff(current_settings, desired_settings) || "(no diff)"
|
186
|
+
end
|
187
|
+
|
188
|
+
def report_action(message)
|
189
|
+
@reporter.report_action(message)
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|