Skip to content

Commit e54b4a7

Browse files
authored
[7.x] Adds write_index_only option to put mapping API (#59539)
1 parent 4d7c59b commit e54b4a7

File tree

6 files changed

+183
-10
lines changed

6 files changed

+183
-10
lines changed

rest-api-spec/src/main/resources/rest-api-spec/api/indices.put_mapping.json

+5
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,11 @@
195195
],
196196
"default":"open",
197197
"description":"Whether to expand wildcard expression to concrete indices that are open, closed or both."
198+
},
199+
"write_index_only":{
200+
"type":"boolean",
201+
"default":false,
202+
"description":"When true, applies mappings only to the write index of an alias or data stream"
198203
}
199204
},
200205
"body":{

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

+17
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ public class PutMappingRequest extends AcknowledgedRequest<PutMappingRequest> im
7878

7979
private Index concreteIndex;
8080

81+
private boolean writeIndexOnly;
82+
8183
public PutMappingRequest(StreamInput in) throws IOException {
8284
super(in);
8385
indices = in.readStringArray();
@@ -93,6 +95,9 @@ public PutMappingRequest(StreamInput in) throws IOException {
9395
} else {
9496
origin = null;
9597
}
98+
if (in.getVersion().onOrAfter(Version.V_7_9_0)) {
99+
writeIndexOnly = in.readBoolean();
100+
}
96101
}
97102

98103
public PutMappingRequest() {
@@ -323,6 +328,15 @@ public PutMappingRequest source(BytesReference mappingSource, XContentType xCont
323328
}
324329
}
325330

331+
public PutMappingRequest writeIndexOnly(boolean writeIndexOnly) {
332+
this.writeIndexOnly = writeIndexOnly;
333+
return this;
334+
}
335+
336+
public boolean writeIndexOnly() {
337+
return writeIndexOnly;
338+
}
339+
326340
@Override
327341
public void writeTo(StreamOutput out) throws IOException {
328342
super.writeTo(out);
@@ -337,6 +351,9 @@ public void writeTo(StreamOutput out) throws IOException {
337351
if (out.getVersion().onOrAfter(Version.V_6_7_0)) {
338352
out.writeOptionalString(origin);
339353
}
354+
if (out.getVersion().onOrAfter(Version.V_7_9_0)) {
355+
out.writeBoolean(writeIndexOnly);
356+
}
340357
}
341358

342359
@Override

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

+21-3
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,9 @@
4242
import org.elasticsearch.transport.TransportService;
4343

4444
import java.io.IOException;
45+
import java.util.ArrayList;
4546
import java.util.Arrays;
47+
import java.util.List;
4648
import java.util.Objects;
4749
import java.util.Optional;
4850

@@ -97,9 +99,8 @@ protected ClusterBlockException checkBlock(PutMappingRequest request, ClusterSta
9799
protected void masterOperation(final PutMappingRequest request, final ClusterState state,
98100
final ActionListener<AcknowledgedResponse> listener) {
99101
try {
100-
final Index[] concreteIndices = request.getConcreteIndex() == null ?
101-
indexNameExpressionResolver.concreteIndices(state, request)
102-
: new Index[] {request.getConcreteIndex()};
102+
final Index[] concreteIndices = resolveIndices(state, request, indexNameExpressionResolver);
103+
103104
final Optional<Exception> maybeValidationException = requestValidators.validateRequest(request, state, concreteIndices);
104105
if (maybeValidationException.isPresent()) {
105106
listener.onFailure(maybeValidationException.get());
@@ -113,6 +114,23 @@ protected void masterOperation(final PutMappingRequest request, final ClusterSta
113114
}
114115
}
115116

117+
static Index[] resolveIndices(final ClusterState state, PutMappingRequest request, final IndexNameExpressionResolver iner) {
118+
if (request.getConcreteIndex() == null) {
119+
if (request.writeIndexOnly()) {
120+
List<Index> indices = new ArrayList<>();
121+
for (String indexExpression : request.indices()) {
122+
indices.add(iner.concreteWriteIndex(state, request.indicesOptions(), indexExpression,
123+
request.indicesOptions().allowNoIndices(), request.includeDataStreams()));
124+
}
125+
return indices.toArray(Index.EMPTY_ARRAY);
126+
} else {
127+
return iner.concreteIndices(state, request);
128+
}
129+
} else {
130+
return new Index[]{request.getConcreteIndex()};
131+
}
132+
}
133+
116134
static void performMappingUpdate(Index[] concreteIndices,
117135
PutMappingRequest request,
118136
ActionListener<AcknowledgedResponse> listener,

server/src/main/java/org/elasticsearch/rest/action/admin/indices/RestPutMappingAction.java

+1
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ public RestChannelConsumer prepareRequest(final RestRequest request, final NodeC
9999
putMappingRequest.timeout(request.paramAsTime("timeout", putMappingRequest.timeout()));
100100
putMappingRequest.masterNodeTimeout(request.paramAsTime("master_timeout", putMappingRequest.masterNodeTimeout()));
101101
putMappingRequest.indicesOptions(IndicesOptions.fromRequest(request, putMappingRequest.indicesOptions()));
102+
putMappingRequest.writeIndexOnly(request.paramAsBoolean("write_index_only", false));
102103
return channel -> client.admin().indices().putMapping(putMappingRequest, new RestToXContentListener<>(channel));
103104
}
104105
}

server/src/test/java/org/elasticsearch/action/admin/indices/mapping/put/PutMappingRequestTests.java

+138
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,16 @@
2020
package org.elasticsearch.action.admin.indices.mapping.put;
2121

2222
import org.elasticsearch.action.ActionRequestValidationException;
23+
import org.elasticsearch.action.admin.indices.datastream.DeleteDataStreamRequestTests;
24+
import org.elasticsearch.cluster.ClusterState;
25+
import org.elasticsearch.cluster.metadata.AliasMetadata;
26+
import org.elasticsearch.cluster.metadata.IndexAbstraction;
27+
import org.elasticsearch.cluster.metadata.IndexMetadata;
28+
import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
29+
import org.elasticsearch.cluster.metadata.Metadata;
2330
import org.elasticsearch.common.Strings;
2431
import org.elasticsearch.common.bytes.BytesReference;
32+
import org.elasticsearch.common.collect.Tuple;
2533
import org.elasticsearch.common.xcontent.XContentBuilder;
2634
import org.elasticsearch.common.xcontent.XContentParser;
2735
import org.elasticsearch.common.xcontent.XContentType;
@@ -31,8 +39,14 @@
3139
import org.elasticsearch.test.ESTestCase;
3240

3341
import java.io.IOException;
42+
import java.util.Arrays;
43+
import java.util.List;
44+
import java.util.stream.Collectors;
3445

46+
import static org.elasticsearch.common.collect.Tuple.tuple;
3547
import static org.elasticsearch.common.xcontent.ToXContent.EMPTY_PARAMS;
48+
import static org.hamcrest.Matchers.containsInAnyOrder;
49+
import static org.hamcrest.Matchers.containsString;
3650

3751
public class PutMappingRequestTests extends ESTestCase {
3852

@@ -75,6 +89,7 @@ public void testBuildFromSimplifiedDef() {
7589
assertEquals("mapping source must be pairs of fieldnames and properties definition.", e.getMessage());
7690
}
7791

92+
7893
public void testToXContent() throws IOException {
7994
PutMappingRequest request = new PutMappingRequest("foo");
8095
request.type("my_type");
@@ -138,4 +153,127 @@ private static PutMappingRequest createTestItem() throws IOException {
138153

139154
return request;
140155
}
156+
157+
public void testResolveIndicesWithWriteIndexOnlyAndDataStreamsAndWriteAliases() {
158+
String[] dataStreamNames = {"foo", "bar", "baz"};
159+
List<Tuple<String, Integer>> dsMetadata = org.elasticsearch.common.collect.List.of(
160+
tuple(dataStreamNames[0], randomIntBetween(1, 3)),
161+
tuple(dataStreamNames[1], randomIntBetween(1, 3)),
162+
tuple(dataStreamNames[2], randomIntBetween(1, 3)));
163+
164+
ClusterState cs = DeleteDataStreamRequestTests.getClusterStateWithDataStreams(dsMetadata,
165+
org.elasticsearch.common.collect.List.of("index1", "index2", "index3"));
166+
cs = addAliases(cs, org.elasticsearch.common.collect.List.of(
167+
tuple("alias1", org.elasticsearch.common.collect.List.of(tuple("index1", false), tuple("index2", true))),
168+
tuple("alias2", org.elasticsearch.common.collect.List.of(tuple("index2", false), tuple("index3", true)))
169+
));
170+
PutMappingRequest request = new PutMappingRequest().indices("foo", "alias1", "alias2").writeIndexOnly(true);
171+
Index[] indices = TransportPutMappingAction.resolveIndices(cs, request, new IndexNameExpressionResolver());
172+
List<String> indexNames = Arrays.stream(indices).map(Index::getName).collect(Collectors.toList());
173+
IndexAbstraction expectedDs = cs.metadata().getIndicesLookup().get("foo");
174+
// should resolve the data stream and each alias to their respective write indices
175+
assertThat(indexNames, containsInAnyOrder(expectedDs.getWriteIndex().getIndex().getName(), "index2", "index3"));
176+
}
177+
178+
public void testResolveIndicesWithoutWriteIndexOnlyAndDataStreamsAndWriteAliases() {
179+
String[] dataStreamNames = {"foo", "bar", "baz"};
180+
List<Tuple<String, Integer>> dsMetadata = org.elasticsearch.common.collect.List.of(
181+
tuple(dataStreamNames[0], randomIntBetween(1, 3)),
182+
tuple(dataStreamNames[1], randomIntBetween(1, 3)),
183+
tuple(dataStreamNames[2], randomIntBetween(1, 3)));
184+
185+
ClusterState cs = DeleteDataStreamRequestTests.getClusterStateWithDataStreams(dsMetadata,
186+
org.elasticsearch.common.collect.List.of("index1", "index2", "index3"));
187+
cs = addAliases(cs, org.elasticsearch.common.collect.List.of(
188+
tuple("alias1", org.elasticsearch.common.collect.List.of(tuple("index1", false), tuple("index2", true))),
189+
tuple("alias2", org.elasticsearch.common.collect.List.of(tuple("index2", false), tuple("index3", true)))
190+
));
191+
PutMappingRequest request = new PutMappingRequest().indices("foo", "alias1", "alias2");
192+
Index[] indices = TransportPutMappingAction.resolveIndices(cs, request, new IndexNameExpressionResolver());
193+
List<String> indexNames = Arrays.stream(indices).map(Index::getName).collect(Collectors.toList());
194+
IndexAbstraction expectedDs = cs.metadata().getIndicesLookup().get("foo");
195+
List<String> expectedIndices = expectedDs.getIndices().stream().map(im -> im.getIndex().getName()).collect(Collectors.toList());
196+
expectedIndices.addAll(org.elasticsearch.common.collect.List.of("index1", "index2", "index3"));
197+
// should resolve the data stream and each alias to _all_ their respective indices
198+
assertThat(indexNames, containsInAnyOrder(expectedIndices.toArray()));
199+
}
200+
201+
public void testResolveIndicesWithWriteIndexOnlyAndDataStreamAndIndex() {
202+
String[] dataStreamNames = {"foo", "bar", "baz"};
203+
List<Tuple<String, Integer>> dsMetadata = org.elasticsearch.common.collect.List.of(
204+
tuple(dataStreamNames[0], randomIntBetween(1, 3)),
205+
tuple(dataStreamNames[1], randomIntBetween(1, 3)),
206+
tuple(dataStreamNames[2], randomIntBetween(1, 3)));
207+
208+
ClusterState cs = DeleteDataStreamRequestTests.getClusterStateWithDataStreams(dsMetadata,
209+
org.elasticsearch.common.collect.List.of("index1", "index2", "index3"));
210+
cs = addAliases(cs, org.elasticsearch.common.collect.List.of(
211+
tuple("alias1", org.elasticsearch.common.collect.List.of(tuple("index1", false), tuple("index2", true))),
212+
tuple("alias2", org.elasticsearch.common.collect.List.of(tuple("index2", false), tuple("index3", true)))
213+
));
214+
PutMappingRequest request = new PutMappingRequest().indices("foo", "index3").writeIndexOnly(true);
215+
Index[] indices = TransportPutMappingAction.resolveIndices(cs, request, new IndexNameExpressionResolver());
216+
List<String> indexNames = Arrays.stream(indices).map(Index::getName).collect(Collectors.toList());
217+
IndexAbstraction expectedDs = cs.metadata().getIndicesLookup().get("foo");
218+
List<String> expectedIndices = expectedDs.getIndices().stream().map(im -> im.getIndex().getName()).collect(Collectors.toList());
219+
expectedIndices.addAll(org.elasticsearch.common.collect.List.of("index1", "index2", "index3"));
220+
// should resolve the data stream and each alias to _all_ their respective indices
221+
assertThat(indexNames, containsInAnyOrder(expectedDs.getWriteIndex().getIndex().getName(), "index3"));
222+
}
223+
224+
public void testResolveIndicesWithWriteIndexOnlyAndNoSingleWriteIndex() {
225+
String[] dataStreamNames = {"foo", "bar", "baz"};
226+
List<Tuple<String, Integer>> dsMetadata = org.elasticsearch.common.collect.List.of(
227+
tuple(dataStreamNames[0], randomIntBetween(1, 3)),
228+
tuple(dataStreamNames[1], randomIntBetween(1, 3)),
229+
tuple(dataStreamNames[2], randomIntBetween(1, 3)));
230+
231+
ClusterState cs = DeleteDataStreamRequestTests.getClusterStateWithDataStreams(dsMetadata,
232+
org.elasticsearch.common.collect.List.of("index1", "index2", "index3"));
233+
final ClusterState cs2 = addAliases(cs, org.elasticsearch.common.collect.List.of(
234+
tuple("alias1", org.elasticsearch.common.collect.List.of(tuple("index1", false), tuple("index2", true))),
235+
tuple("alias2", org.elasticsearch.common.collect.List.of(tuple("index2", false), tuple("index3", true)))
236+
));
237+
PutMappingRequest request = new PutMappingRequest().indices("*").writeIndexOnly(true);
238+
IllegalArgumentException e = expectThrows(IllegalArgumentException.class,
239+
() -> TransportPutMappingAction.resolveIndices(cs2, request, new IndexNameExpressionResolver()));
240+
assertThat(e.getMessage(), containsString("The index expression [*] and options provided did not point to a single write-index"));
241+
}
242+
243+
public void testResolveIndicesWithWriteIndexOnlyAndAliasWithoutWriteIndex() {
244+
String[] dataStreamNames = {"foo", "bar", "baz"};
245+
List<Tuple<String, Integer>> dsMetadata = org.elasticsearch.common.collect.List.of(
246+
tuple(dataStreamNames[0], randomIntBetween(1, 3)),
247+
tuple(dataStreamNames[1], randomIntBetween(1, 3)),
248+
tuple(dataStreamNames[2], randomIntBetween(1, 3)));
249+
250+
ClusterState cs = DeleteDataStreamRequestTests.getClusterStateWithDataStreams(dsMetadata,
251+
org.elasticsearch.common.collect.List.of("index1", "index2", "index3"));
252+
final ClusterState cs2 = addAliases(cs, org.elasticsearch.common.collect.List.of(
253+
tuple("alias1", org.elasticsearch.common.collect.List.of(tuple("index1", false), tuple("index2", false))),
254+
tuple("alias2", org.elasticsearch.common.collect.List.of(tuple("index2", false), tuple("index3", false)))
255+
));
256+
PutMappingRequest request = new PutMappingRequest().indices("alias2").writeIndexOnly(true);
257+
IllegalArgumentException e = expectThrows(IllegalArgumentException.class,
258+
() -> TransportPutMappingAction.resolveIndices(cs2, request, new IndexNameExpressionResolver()));
259+
assertThat(e.getMessage(), containsString("no write index is defined for alias [alias2]"));
260+
}
261+
262+
/**
263+
* Adds aliases to the supplied ClusterState instance. The aliases parameter takes of list of tuples of aliasName
264+
* to the alias's indices. The alias's indices are a tuple of index name and a flag indicating whether the alias
265+
* is a write alias for that index. See usage examples above.
266+
*/
267+
private static ClusterState addAliases(ClusterState cs, List<Tuple<String, List<Tuple<String, Boolean>>>> aliases) {
268+
Metadata.Builder builder = Metadata.builder(cs.metadata());
269+
for (Tuple<String, List<Tuple<String, Boolean>>> alias : aliases) {
270+
for (Tuple<String, Boolean> index : alias.v2()) {
271+
IndexMetadata im = builder.get(index.v1());
272+
AliasMetadata newAliasMd = AliasMetadata.newAliasMetadataBuilder(alias.v1()).writeIndex(index.v2()).build();
273+
builder.put(IndexMetadata.builder(im).putAlias(newAliasMd));
274+
}
275+
}
276+
return ClusterState.builder(cs).metadata(builder.build()).build();
277+
}
278+
141279
}

x-pack/plugin/src/test/resources/rest-api-spec/test/security/authz/50_data_streams.yml

+1-7
Original file line numberDiff line numberDiff line change
@@ -320,13 +320,7 @@ teardown:
320320
name: my-template1
321321
body:
322322
index_patterns: [simple*]
323-
template:
324-
mappings:
325-
properties:
326-
'@timestamp':
327-
type: date
328-
data_stream:
329-
timestamp_field: '@timestamp'
323+
data_stream: {}
330324

331325
- do:
332326
security.put_role:

0 commit comments

Comments
 (0)