Skip to content

Commit dc1856c

Browse files
committed
Make sure to validate the type before attempting to merge a new mapping. (#45157)
Currently, when adding a new mapping, we attempt to parse + merge it before checking whether its top-level document type matches the existing type. So when a user attempts to introduce a new mapping type, we may give a confusing error message around merging instead of complaining that it's not possible to add more than one type ("Rejecting mapping update to [my-index] as the final mapping would have more than 1 type..."). This PR moves the type validation to the start of `MetaDataMappingService#applyRequest` so that we make sure the type matches before performing any mapper merging. We already partially addressed this issue in #29316, but the tests there focused on `MapperService` and did not catch this problem with end-to-end mapping updates. Addresses #43012.
1 parent 4d97d2c commit dc1856c

File tree

4 files changed

+95
-49
lines changed

4 files changed

+95
-49
lines changed

server/src/main/java/org/elasticsearch/cluster/metadata/MetaDataMappingService.java

+8-8
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,13 @@ private ClusterState applyRequest(ClusterState currentState, PutMappingClusterSt
265265
// try and parse it (no need to add it here) so we can bail early in case of parsing exception
266266
DocumentMapper newMapper;
267267
DocumentMapper existingMapper = mapperService.documentMapper();
268+
269+
String typeForUpdate = mapperService.getTypeForUpdate(mappingType, mappingUpdateSource);
270+
if (existingMapper != null && existingMapper.type().equals(typeForUpdate) == false) {
271+
throw new IllegalArgumentException("Rejecting mapping update to [" + mapperService.index().getName() +
272+
"] as the final mapping would have more than 1 type: " + Arrays.asList(existingMapper.type(), typeForUpdate));
273+
}
274+
268275
if (MapperService.DEFAULT_MAPPING.equals(request.type())) {
269276
// _default_ types do not go through merging, but we do test the new settings. Also don't apply the old default
270277
newMapper = mapperService.parse(request.type(), mappingUpdateSource, false);
@@ -299,14 +306,7 @@ private ClusterState applyRequest(ClusterState currentState, PutMappingClusterSt
299306
final Index index = indexMetaData.getIndex();
300307
final MapperService mapperService = indexMapperServices.get(index);
301308

302-
// If the _type name is _doc and there is no _doc top-level key then this means that we
303-
// are handling a typeless call. In such a case, we override _doc with the actual type
304-
// name in the mappings. This allows to use typeless APIs on typed indices.
305-
String typeForUpdate = mappingType; // the type to use to apply the mapping update
306-
if (isMappingSourceTyped(request.type(), mappingUpdateSource) == false) {
307-
typeForUpdate = mapperService.resolveDocumentType(mappingType);
308-
}
309-
309+
String typeForUpdate = mapperService.getTypeForUpdate(mappingType, mappingUpdateSource);
310310
CompressedXContent existingSource = null;
311311
DocumentMapper existingMapper = mapperService.documentMapper(typeForUpdate);
312312
if (existingMapper != null) {

server/src/main/java/org/elasticsearch/index/mapper/MapperService.java

+9-10
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121

2222
import com.carrotsearch.hppc.ObjectHashSet;
2323
import com.carrotsearch.hppc.cursors.ObjectCursor;
24-
2524
import org.apache.logging.log4j.LogManager;
2625
import org.apache.logging.log4j.message.ParameterizedMessage;
2726
import org.apache.lucene.analysis.Analyzer;
@@ -72,7 +71,6 @@
7271
import java.util.LinkedHashMap;
7372
import java.util.List;
7473
import java.util.Map;
75-
import java.util.Objects;
7674
import java.util.Set;
7775
import java.util.function.Function;
7876
import java.util.function.Supplier;
@@ -452,14 +450,6 @@ private synchronized Map<String, DocumentMapper> internalMerge(@Nullable Documen
452450
results.put(DEFAULT_MAPPING, defaultMapper);
453451
}
454452

455-
{
456-
if (mapper != null && this.mapper != null && Objects.equals(this.mapper.type(), mapper.type()) == false) {
457-
throw new IllegalArgumentException(
458-
"Rejecting mapping update to [" + index().getName() + "] as the final mapping would have more than 1 type: "
459-
+ Arrays.asList(this.mapper.type(), mapper.type()));
460-
}
461-
}
462-
463453
DocumentMapper newMapper = null;
464454
if (mapper != null) {
465455
// check naming
@@ -707,6 +697,15 @@ public static boolean isMappingSourceTyped(String type, CompressedXContent mappi
707697
return isMappingSourceTyped(type, root);
708698
}
709699

700+
/**
701+
* If the _type name is _doc and there is no _doc top-level key then this means that we
702+
* are handling a typeless call. In such a case, we override _doc with the actual type
703+
* name in the mappings. This allows to use typeless APIs on typed indices.
704+
*/
705+
public String getTypeForUpdate(String type, CompressedXContent mappingSource) {
706+
return isMappingSourceTyped(type, mappingSource) == false ? resolveDocumentType(type) : type;
707+
}
708+
710709
/**
711710
* Resolves a type from a mapping-related request into the type that should be used when
712711
* merging and updating mappings.

server/src/test/java/org/elasticsearch/cluster/metadata/MetaDataMappingServiceTests.java

+78
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,15 @@
1919

2020
package org.elasticsearch.cluster.metadata;
2121

22+
import org.elasticsearch.action.admin.indices.create.CreateIndexRequestBuilder;
2223
import org.elasticsearch.action.admin.indices.mapping.put.PutMappingClusterStateUpdateRequest;
2324
import org.elasticsearch.cluster.ClusterState;
2425
import org.elasticsearch.cluster.ClusterStateTaskExecutor;
2526
import org.elasticsearch.cluster.service.ClusterService;
27+
import org.elasticsearch.common.Strings;
2628
import org.elasticsearch.common.compress.CompressedXContent;
29+
import org.elasticsearch.common.xcontent.XContentBuilder;
30+
import org.elasticsearch.common.xcontent.XContentFactory;
2731
import org.elasticsearch.index.Index;
2832
import org.elasticsearch.index.IndexService;
2933
import org.elasticsearch.index.mapper.MapperService;
@@ -34,6 +38,7 @@
3438
import java.util.Collection;
3539
import java.util.Collections;
3640

41+
import static org.hamcrest.CoreMatchers.containsString;
3742
import static org.hamcrest.Matchers.equalTo;
3843
import static org.hamcrest.Matchers.not;
3944

@@ -134,4 +139,77 @@ public void testMappingUpdateAccepts_docAsType() throws Exception {
134139
Collections.singletonMap("foo",
135140
Collections.singletonMap("type", "keyword"))), mappingMetaData.sourceAsMap());
136141
}
142+
143+
public void testForbidMultipleTypes() throws Exception {
144+
CreateIndexRequestBuilder createIndexRequest = client().admin().indices()
145+
.prepareCreate("test")
146+
.addMapping(MapperService.SINGLE_MAPPING_NAME);
147+
IndexService indexService = createIndex("test", createIndexRequest);
148+
149+
MetaDataMappingService mappingService = getInstanceFromNode(MetaDataMappingService.class);
150+
ClusterService clusterService = getInstanceFromNode(ClusterService.class);
151+
152+
PutMappingClusterStateUpdateRequest request = new PutMappingClusterStateUpdateRequest()
153+
.type("other_type")
154+
.indices(new Index[] {indexService.index()})
155+
.source(Strings.toString(XContentFactory.jsonBuilder()
156+
.startObject()
157+
.startObject("other_type").endObject()
158+
.endObject()));
159+
ClusterStateTaskExecutor.ClusterTasksResult<PutMappingClusterStateUpdateRequest> result =
160+
mappingService.putMappingExecutor.execute(clusterService.state(), Collections.singletonList(request));
161+
assertThat(result.executionResults.size(), equalTo(1));
162+
163+
ClusterStateTaskExecutor.TaskResult taskResult = result.executionResults.values().iterator().next();
164+
assertFalse(taskResult.isSuccess());
165+
assertThat(taskResult.getFailure().getMessage(), containsString(
166+
"Rejecting mapping update to [test] as the final mapping would have more than 1 type: "));
167+
}
168+
169+
/**
170+
* This test checks that the multi-type validation is done before we do any other kind of validation
171+
* on the mapping that's added, see https://github.com/elastic/elasticsearch/issues/29313
172+
*/
173+
public void testForbidMultipleTypesWithConflictingMappings() throws Exception {
174+
XContentBuilder mapping = XContentFactory.jsonBuilder().startObject()
175+
.startObject(MapperService.SINGLE_MAPPING_NAME)
176+
.startObject("properties")
177+
.startObject("field1")
178+
.field("type", "text")
179+
.endObject()
180+
.endObject()
181+
.endObject()
182+
.endObject();
183+
184+
CreateIndexRequestBuilder createIndexRequest = client().admin().indices()
185+
.prepareCreate("test")
186+
.addMapping(MapperService.SINGLE_MAPPING_NAME, mapping);
187+
IndexService indexService = createIndex("test", createIndexRequest);
188+
189+
MetaDataMappingService mappingService = getInstanceFromNode(MetaDataMappingService.class);
190+
ClusterService clusterService = getInstanceFromNode(ClusterService.class);
191+
192+
String conflictingMapping = Strings.toString(XContentFactory.jsonBuilder().startObject()
193+
.startObject("other_type")
194+
.startObject("properties")
195+
.startObject("field1")
196+
.field("type", "keyword")
197+
.endObject()
198+
.endObject()
199+
.endObject()
200+
.endObject());
201+
202+
PutMappingClusterStateUpdateRequest request = new PutMappingClusterStateUpdateRequest()
203+
.type("other_type")
204+
.indices(new Index[] {indexService.index()})
205+
.source(conflictingMapping);
206+
ClusterStateTaskExecutor.ClusterTasksResult<PutMappingClusterStateUpdateRequest> result =
207+
mappingService.putMappingExecutor.execute(clusterService.state(), Collections.singletonList(request));
208+
assertThat(result.executionResults.size(), equalTo(1));
209+
210+
ClusterStateTaskExecutor.TaskResult taskResult = result.executionResults.values().iterator().next();
211+
assertFalse(taskResult.isSuccess());
212+
assertThat(taskResult.getFailure().getMessage(), containsString(
213+
"Rejecting mapping update to [test] as the final mapping would have more than 1 type: "));
214+
}
137215
}

server/src/test/java/org/elasticsearch/index/mapper/MapperServiceTests.java

-31
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@
5757

5858
import static org.hamcrest.CoreMatchers.containsString;
5959
import static org.hamcrest.Matchers.instanceOf;
60-
import static org.hamcrest.Matchers.startsWith;
6160

6261
public class MapperServiceTests extends ESSingleNodeTestCase {
6362

@@ -298,36 +297,6 @@ public void testTotalFieldsLimitWithFieldAlias() throws Throwable {
298297
assertEquals("Limit of total fields [" + numberOfNonAliasFields + "] in index [test2] has been exceeded", e.getMessage());
299298
}
300299

301-
public void testForbidMultipleTypes() throws IOException {
302-
String mapping = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("type").endObject().endObject());
303-
MapperService mapperService = createIndex("test").mapperService();
304-
mapperService.merge("type", new CompressedXContent(mapping), MergeReason.MAPPING_UPDATE);
305-
306-
String mapping2 = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("type2").endObject().endObject());
307-
IllegalArgumentException e = expectThrows(IllegalArgumentException.class,
308-
() -> mapperService.merge("type2", new CompressedXContent(mapping2), MergeReason.MAPPING_UPDATE));
309-
assertThat(e.getMessage(), startsWith("Rejecting mapping update to [test] as the final mapping would have more than 1 type: "));
310-
}
311-
312-
/**
313-
* This test checks that the multi-type validation is done before we do any other kind of validation on the mapping that's added,
314-
* see https://github.com/elastic/elasticsearch/issues/29313
315-
*/
316-
public void testForbidMultipleTypesWithConflictingMappings() throws IOException {
317-
String mapping = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("type")
318-
.startObject("properties").startObject("field1").field("type", "integer_range")
319-
.endObject().endObject().endObject().endObject());
320-
MapperService mapperService = createIndex("test").mapperService();
321-
mapperService.merge("type", new CompressedXContent(mapping), MergeReason.MAPPING_UPDATE);
322-
323-
String mapping2 = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("type2")
324-
.startObject("properties").startObject("field1").field("type", "integer")
325-
.endObject().endObject().endObject().endObject());
326-
IllegalArgumentException e = expectThrows(IllegalArgumentException.class,
327-
() -> mapperService.merge("type2", new CompressedXContent(mapping2), MergeReason.MAPPING_UPDATE));
328-
assertThat(e.getMessage(), startsWith("Rejecting mapping update to [test] as the final mapping would have more than 1 type: "));
329-
}
330-
331300
public void testDefaultMappingIsRejectedOn7() throws IOException {
332301
String mapping = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("_default_").endObject().endObject());
333302
MapperService mapperService = createIndex("test").mapperService();

0 commit comments

Comments
 (0)