Skip to content

Commit ae94701

Browse files
authored
Migrate watcher to system indices infrastructure (#67588)
Part of #61656. Migrate the `.watches` and `.triggered_watches` system indices to use the auto-create infrastructure. The watcher history indices are left alone. As part of this work, a `SystemIndexDescriptor` now inspects its mappings to determine whether it has any dynamic mappings. This influences how strict Elasticsearch is with enforcing the descriptor's mappings, since ES cannot know in advanced what all the mappings will be. This PR also fixes the `SystemIndexManager` so that (1) it doesn't fall over when attempting to inspect the state of an index that hasn't been created yet, and (2) does update mappings if there's no version field in the mapping metadata.
1 parent 69309c1 commit ae94701

File tree

15 files changed

+315
-277
lines changed

15 files changed

+315
-277
lines changed

server/src/main/java/org/elasticsearch/action/admin/indices/mapping/put/TransportPutMappingAction.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ static String checkForSystemIndexViolations(SystemIndices systemIndices, Index[]
158158

159159
for (Index index : concreteIndices) {
160160
final SystemIndexDescriptor descriptor = systemIndices.findMatchingDescriptor(index.getName());
161-
if (descriptor != null && descriptor.isAutomaticallyManaged()) {
161+
if (descriptor != null && descriptor.isAutomaticallyManaged() && descriptor.hasDynamicMappings() == false) {
162162
final String descriptorMappings = descriptor.getMappings();
163163
// Technically we could trip over a difference in whitespace here, but then again nobody should be trying to manually
164164
// update a descriptor's mappings.

server/src/main/java/org/elasticsearch/indices/SystemIndexDescriptor.java

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,14 @@
1818
import org.elasticsearch.common.Strings;
1919
import org.elasticsearch.common.settings.Settings;
2020
import org.elasticsearch.common.xcontent.XContentBuilder;
21+
import org.elasticsearch.common.xcontent.XContentHelper;
22+
import org.elasticsearch.common.xcontent.json.JsonXContent;
2123

2224
import java.util.ArrayList;
2325
import java.util.Collections;
2426
import java.util.List;
2527
import java.util.Locale;
28+
import java.util.Map;
2629
import java.util.Objects;
2730

2831
/**
@@ -71,6 +74,9 @@ public class SystemIndexDescriptor {
7174
/** The minimum cluster node version required for this descriptor, or null if there is no restriction */
7275
private final Version minimumNodeVersion;
7376

77+
/** Whether there are dynamic fields in this descriptor's mappings */
78+
private final boolean hasDynamicMappings;
79+
7480
/**
7581
* Creates a descriptor for system indices matching the supplied pattern. These indices will not be managed
7682
* by Elasticsearch internally.
@@ -166,8 +172,12 @@ public SystemIndexDescriptor(String indexPattern, String description) {
166172
this.versionMetaKey = versionMetaKey;
167173
this.origin = origin;
168174
this.minimumNodeVersion = minimumNodeVersion;
175+
176+
this.hasDynamicMappings = this.mappings != null
177+
&& findDynamicMapping(XContentHelper.convertToMap(JsonXContent.jsonXContent, mappings, false));
169178
}
170179

180+
171181
/**
172182
* @return The pattern of index names that this descriptor will be used for.
173183
*/
@@ -252,6 +262,10 @@ public String getOrigin() {
252262
return this.origin;
253263
}
254264

265+
public boolean hasDynamicMappings() {
266+
return this.hasDynamicMappings;
267+
}
268+
255269
/**
256270
* Checks that this descriptor can be used within this cluster, by comparing the supplied minimum
257271
* node version to this descriptor's minimum version.
@@ -410,4 +424,32 @@ private static String patternToRegex(String input) {
410424
output = output.replaceAll("\\*", ".*");
411425
return output;
412426
}
427+
428+
/**
429+
* Recursively searches for <code>dynamic: true</code> in the supplies mappings
430+
* @param map a parsed fragment of an index's mappings
431+
* @return whether the fragment contains a dynamic mapping
432+
*/
433+
@SuppressWarnings("unchecked")
434+
static boolean findDynamicMapping(Map<String, Object> map) {
435+
if (map == null) {
436+
return false;
437+
}
438+
439+
for (Map.Entry<String, Object> entry : map.entrySet()) {
440+
final String key = entry.getKey();
441+
final Object value = entry.getValue();
442+
if (key.equals("dynamic") && (value instanceof Boolean) && ((Boolean) value)) {
443+
return true;
444+
}
445+
446+
if (value instanceof Map) {
447+
if (findDynamicMapping((Map<String, Object>) value)) {
448+
return true;
449+
}
450+
}
451+
}
452+
453+
return false;
454+
}
413455
}

server/src/main/java/org/elasticsearch/indices/SystemIndexManager.java

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@ public class SystemIndexManager implements ClusterStateListener {
5252
private final Client client;
5353
private final AtomicBoolean isUpgradeInProgress;
5454

55+
/**
56+
* Creates a new manager
57+
* @param systemIndices the indices to manage
58+
* @param client used to update the cluster
59+
*/
5560
public SystemIndexManager(SystemIndices systemIndices, Client client) {
5661
this.systemIndices = systemIndices;
5762
this.client = client;
@@ -138,6 +143,11 @@ UpgradeStatus getUpgradeStatus(ClusterState clusterState, SystemIndexDescriptor
138143
// The messages below will be logged on every cluster state update, which is why even in the index closed / red
139144
// cases, the log levels are DEBUG.
140145

146+
if (indexState == null) {
147+
logger.debug("Index {} does not exist yet", indexDescription);
148+
return UpgradeStatus.UP_TO_DATE;
149+
}
150+
141151
if (indexState.indexState == IndexMetadata.State.CLOSE) {
142152
logger.debug("Index {} is closed. This is likely to prevent some features from functioning correctly", indexDescription);
143153
return UpgradeStatus.CLOSED;
@@ -198,10 +208,16 @@ public void onFailure(Exception e) {
198208

199209
/**
200210
* Derives a summary of the current state of a system index, relative to the given cluster state.
211+
* @param state the cluster state from which to derive the index state
212+
* @param descriptor the system index to check
213+
* @return a summary of the index state, or <code>null</code> if the index doesn't exist
201214
*/
202215
State calculateIndexState(ClusterState state, SystemIndexDescriptor descriptor) {
203216
final IndexMetadata indexMetadata = state.metadata().index(descriptor.getPrimaryIndex());
204-
assert indexMetadata != null;
217+
218+
if (indexMetadata == null) {
219+
return null;
220+
}
205221

206222
final boolean isIndexUpToDate = INDEX_FORMAT_SETTING.get(indexMetadata.getSettings()) == descriptor.getIndexFormat();
207223

@@ -255,6 +271,8 @@ private Version readMappingVersion(SystemIndexDescriptor descriptor, MappingMeta
255271
final String versionString = (String) meta.get(descriptor.getVersionMetaKey());
256272
if (versionString == null) {
257273
logger.warn("No value found in mappings for [_meta.{}]", descriptor.getVersionMetaKey());
274+
// If we called `Version.fromString(null)`, it would return `Version.CURRENT` and we wouldn't update the mappings
275+
return Version.V_EMPTY;
258276
}
259277
return Version.fromString(versionString);
260278
} catch (ElasticsearchParseException e) {

server/src/test/java/org/elasticsearch/indices/SystemIndexDescriptorTests.java

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,21 @@
88

99
package org.elasticsearch.indices;
1010

11+
import org.elasticsearch.common.xcontent.XContentHelper;
12+
import org.elasticsearch.common.xcontent.json.JsonXContent;
1113
import org.elasticsearch.test.ESTestCase;
1214

15+
import java.util.Map;
16+
17+
import static org.elasticsearch.indices.SystemIndexDescriptor.findDynamicMapping;
1318
import static org.hamcrest.Matchers.containsString;
1419
import static org.hamcrest.Matchers.equalTo;
1520

1621
public class SystemIndexDescriptorTests extends ESTestCase {
1722

23+
/**
24+
* Tests the various validation rules that are applied when creating a new system index descriptor.
25+
*/
1826
public void testValidation() {
1927
{
2028
Exception ex = expectThrows(NullPointerException.class,
@@ -75,4 +83,35 @@ public void testValidation() {
7583
);
7684
}
7785
}
86+
87+
/**
88+
* Check that a system index descriptor correctly identifies the presence of a dynamic mapping when once is present.
89+
*/
90+
public void testFindDynamicMappingsWithDynamicMapping() {
91+
String json = "{"
92+
+ " \"foo\": {"
93+
+ " \"bar\": {"
94+
+ " \"dynamic\": false"
95+
+ " },"
96+
+ " \"baz\": {"
97+
+ " \"dynamic\": true"
98+
+ " }"
99+
+ " }"
100+
+ "}";
101+
102+
final Map<String, Object> mappings = XContentHelper.convertToMap(JsonXContent.jsonXContent, json, false);
103+
104+
assertThat(findDynamicMapping(mappings), equalTo(true));
105+
}
106+
107+
/**
108+
* Check that a system index descriptor correctly identifies the absence of a dynamic mapping when none are present.
109+
*/
110+
public void testFindDynamicMappingsWithoutDynamicMapping() {
111+
String json = "{ \"foo\": { \"bar\": { \"dynamic\": false } } }";
112+
113+
final Map<String, Object> mappings = XContentHelper.convertToMap(JsonXContent.jsonXContent, json, false);
114+
115+
assertThat(findDynamicMapping(mappings), equalTo(false));
116+
}
78117
}

server/src/test/java/org/elasticsearch/indices/SystemIndexManagerTests.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,19 @@ public void testManagerProcessesIndicesWithOutdatedMappings() {
216216
assertThat(manager.getUpgradeStatus(clusterStateBuilder.build(), DESCRIPTOR), equalTo(UpgradeStatus.NEEDS_MAPPINGS_UPDATE));
217217
}
218218

219+
/**
220+
* Check that the manager will try to upgrade indices where the version in the metadata is null or absent.
221+
*/
222+
public void testManagerProcessesIndicesWithNullVersionMetadata() {
223+
SystemIndices systemIndices = new SystemIndices(Map.of("MyIndex", FEATURE));
224+
SystemIndexManager manager = new SystemIndexManager(systemIndices, client);
225+
226+
final ClusterState.Builder clusterStateBuilder = createClusterState(Strings.toString(getMappings(null)));
227+
markShardsAvailable(clusterStateBuilder);
228+
229+
assertThat(manager.getUpgradeStatus(clusterStateBuilder.build(), DESCRIPTOR), equalTo(UpgradeStatus.NEEDS_MAPPINGS_UPDATE));
230+
}
231+
219232
/**
220233
* Check that the manager submits the expected request for an index whose mappings are out-of-date.
221234
*/

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/watcher/support/WatcherIndexTemplateRegistryField.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,16 @@ public final class WatcherIndexTemplateRegistryField {
2020
// version 12: templates changed to composable templates
2121
// version 13: add `allow_auto_create` setting
2222
// version 14: move watch history to data stream
23+
// version 15: remove watches and triggered watches, these are now system indices
2324
// Note: if you change this, also inform the kibana team around the watcher-ui
2425
public static final int INDEX_TEMPLATE_VERSION = 14;
2526
public static final String HISTORY_TEMPLATE_NAME = ".watch-history-" + INDEX_TEMPLATE_VERSION;
2627
public static final String HISTORY_TEMPLATE_NAME_NO_ILM = ".watch-history-no-ilm-" + INDEX_TEMPLATE_VERSION;
27-
public static final String TRIGGERED_TEMPLATE_NAME = ".triggered_watches";
28-
public static final String WATCHES_TEMPLATE_NAME = ".watches";
2928
public static final String[] TEMPLATE_NAMES = new String[] {
30-
HISTORY_TEMPLATE_NAME, TRIGGERED_TEMPLATE_NAME, WATCHES_TEMPLATE_NAME
29+
HISTORY_TEMPLATE_NAME
3130
};
3231
public static final String[] TEMPLATE_NAMES_NO_ILM = new String[] {
33-
HISTORY_TEMPLATE_NAME_NO_ILM, TRIGGERED_TEMPLATE_NAME, WATCHES_TEMPLATE_NAME
32+
HISTORY_TEMPLATE_NAME_NO_ILM
3433
};
3534

3635
private WatcherIndexTemplateRegistryField() {}

x-pack/plugin/core/src/main/resources/triggered-watches.json

Lines changed: 0 additions & 46 deletions
This file was deleted.

x-pack/plugin/core/src/main/resources/watches.json

Lines changed: 0 additions & 68 deletions
This file was deleted.

x-pack/plugin/watcher/src/internalClusterTest/java/org/elasticsearch/xpack/watcher/test/AbstractWatcherIntegrationTestCase.java

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,6 @@
8989
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked;
9090
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertFutureThrows;
9191
import static org.elasticsearch.xpack.core.watcher.support.WatcherIndexTemplateRegistryField.HISTORY_TEMPLATE_NAME;
92-
import static org.elasticsearch.xpack.core.watcher.support.WatcherIndexTemplateRegistryField.TRIGGERED_TEMPLATE_NAME;
93-
import static org.elasticsearch.xpack.core.watcher.support.WatcherIndexTemplateRegistryField.WATCHES_TEMPLATE_NAME;
9492
import static org.hamcrest.Matchers.emptyArray;
9593
import static org.hamcrest.Matchers.equalTo;
9694
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
@@ -478,12 +476,6 @@ private void ensureWatcherTemplatesAdded() throws Exception {
478476
GetComposableIndexTemplateAction.Response response = client().execute(GetComposableIndexTemplateAction.INSTANCE,
479477
new GetComposableIndexTemplateAction.Request(HISTORY_TEMPLATE_NAME)).get();
480478
assertThat("[" + HISTORY_TEMPLATE_NAME + "] is missing", response.indexTemplates().size(), equalTo(1));
481-
response = client().execute(GetComposableIndexTemplateAction.INSTANCE,
482-
new GetComposableIndexTemplateAction.Request(TRIGGERED_TEMPLATE_NAME)).get();
483-
assertThat("[" + TRIGGERED_TEMPLATE_NAME + "] is missing", response.indexTemplates().size(), equalTo(1));
484-
response = client().execute(GetComposableIndexTemplateAction.INSTANCE,
485-
new GetComposableIndexTemplateAction.Request(WATCHES_TEMPLATE_NAME)).get();
486-
assertThat("[" + WATCHES_TEMPLATE_NAME + "] is missing", response.indexTemplates().size(), equalTo(1));
487479
});
488480
}
489481

0 commit comments

Comments
 (0)