Skip to content

Commit

Permalink
Add rest endpoint for create_from_source_index (elastic#119250)
Browse files Browse the repository at this point in the history
Add rest endpoint which creates a new index using the settings and mappings from an existing source index. Setting and mapping overrides can be added to the request which will override settings and mappings from the source index.

Example: `PUT /_create_from/{source}/{dest}`
Content is optional but can include a `settings_override` and `mappings_override` which will be combined with the settings and mappings from the source index to create the destination index.
  • Loading branch information
parkertimmins authored Jan 8, 2025
1 parent c3839e1 commit b34e278
Show file tree
Hide file tree
Showing 9 changed files with 367 additions and 15 deletions.
5 changes: 5 additions & 0 deletions docs/changelog/119250.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pr: 119250
summary: Add rest endpoint for `create_from_source_index`
area: Data streams
type: enhancement
issues: []
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"migrate.create_from":{
"documentation":{
"url":"https://www.elastic.co/guide/en/elasticsearch/reference/master/data-stream-reindex.html",
"description":"This API creates a destination from a source index. It copies the mappings and settings from the source index while allowing request settings and mappings to override the source values."
},
"stability":"experimental",
"visibility":"private",
"headers":{
"accept": [ "application/json"],
"content_type": ["application/json"]
},
"url":{
"paths":[
{
"path":"/_create_from/{source}/{dest}",
"methods":[ "PUT", "POST"],
"parts":{
"source":{
"type":"string",
"description":"The source index name"
},
"dest":{
"type":"string",
"description":"The destination index name"
}
}
}
]
},
"body":{
"description":"The body contains the fields `mappings_override` and `settings_override`.",
"required":false
}
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
import org.elasticsearch.xpack.migrate.action.ReindexDataStreamIndexTransportAction;
import org.elasticsearch.xpack.migrate.action.ReindexDataStreamTransportAction;
import org.elasticsearch.xpack.migrate.rest.RestCancelReindexDataStreamAction;
import org.elasticsearch.xpack.migrate.rest.RestCreateIndexFromSourceAction;
import org.elasticsearch.xpack.migrate.rest.RestGetMigrationReindexStatusAction;
import org.elasticsearch.xpack.migrate.rest.RestMigrationReindexAction;
import org.elasticsearch.xpack.migrate.task.ReindexDataStreamPersistentTaskExecutor;
Expand Down Expand Up @@ -77,6 +78,7 @@ public List<RestHandler> getRestHandlers(
handlers.add(new RestMigrationReindexAction());
handlers.add(new RestGetMigrationReindexStatusAction());
handlers.add(new RestCancelReindexDataStreamAction());
handlers.add(new RestCreateIndexFromSourceAction());
}
return handlers;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.xcontent.ObjectParser;
import org.elasticsearch.xcontent.ParseField;
import org.elasticsearch.xcontent.ToXContent;
import org.elasticsearch.xcontent.XContentBuilder;
import org.elasticsearch.xcontent.XContentParser;

import java.io.IOException;
import java.util.Map;
Expand All @@ -30,18 +35,35 @@ private CreateIndexFromSourceAction() {
super(NAME);
}

public static class Request extends ActionRequest implements IndicesRequest {

public static class Request extends ActionRequest implements IndicesRequest, ToXContent {
private final String sourceIndex;
private final String destIndex;
private final Settings settingsOverride;
private final Map<String, Object> mappingsOverride;
private Settings settingsOverride = Settings.EMPTY;
private Map<String, Object> mappingsOverride = Map.of();
private static final ParseField SETTINGS_OVERRIDE_FIELD = new ParseField("settings_override");
private static final ParseField MAPPINGS_OVERRIDE_FIELD = new ParseField("mappings_override");
private static final ObjectParser<Request, Void> PARSER = new ObjectParser<>("create_index_from_source_request");

static {
PARSER.declareField(
(parser, request, context) -> request.settingsOverride(Settings.fromXContent(parser)),
SETTINGS_OVERRIDE_FIELD,
ObjectParser.ValueType.OBJECT
);

PARSER.declareField(
(parser, request, context) -> request.mappingsOverride(Map.of("_doc", parser.map())),
MAPPINGS_OVERRIDE_FIELD,
ObjectParser.ValueType.OBJECT
);
}

public Request(String sourceIndex, String destIndex) {
this(sourceIndex, destIndex, Settings.EMPTY, Map.of());
}

public Request(String sourceIndex, String destIndex, Settings settingsOverride, Map<String, Object> mappingsOverride) {
Objects.requireNonNull(settingsOverride);
Objects.requireNonNull(mappingsOverride);
this.sourceIndex = sourceIndex;
this.destIndex = destIndex;
Expand Down Expand Up @@ -72,22 +94,52 @@ public ActionRequestValidationException validate() {
return null;
}

public String getSourceIndex() {
public String sourceIndex() {
return sourceIndex;
}

public String getDestIndex() {
public String destIndex() {
return destIndex;
}

public Settings getSettingsOverride() {
public Settings settingsOverride() {
return settingsOverride;
}

public Map<String, Object> getMappingsOverride() {
public Map<String, Object> mappingsOverride() {
return mappingsOverride;
}

public void settingsOverride(Settings settingsOverride) {
this.settingsOverride = settingsOverride;
}

public void mappingsOverride(Map<String, Object> mappingsOverride) {
this.mappingsOverride = mappingsOverride;
}

public void fromXContent(XContentParser parser) throws IOException {
PARSER.parse(parser, this, null);
}

/*
* This only exists for the sake of testing the xcontent parser
*/
@Override
public XContentBuilder toXContent(XContentBuilder builder, ToXContent.Params params) throws IOException {
if (mappingsOverride.containsKey("_doc")) {
builder.field(MAPPINGS_OVERRIDE_FIELD.getPreferredName(), mappingsOverride.get("_doc"));
}

if (settingsOverride.isEmpty() == false) {
builder.startObject(SETTINGS_OVERRIDE_FIELD.getPreferredName());
settingsOverride.toXContent(builder, params);
builder.endObject();
}

return builder;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,31 +69,31 @@ public CreateIndexFromSourceTransportAction(
@Override
protected void doExecute(Task task, CreateIndexFromSourceAction.Request request, ActionListener<AcknowledgedResponse> listener) {

IndexMetadata sourceIndex = clusterService.state().getMetadata().index(request.getSourceIndex());
IndexMetadata sourceIndex = clusterService.state().getMetadata().index(request.sourceIndex());

if (sourceIndex == null) {
listener.onFailure(new IndexNotFoundException(request.getSourceIndex()));
listener.onFailure(new IndexNotFoundException(request.sourceIndex()));
return;
}

logger.debug("Creating destination index [{}] for source index [{}]", request.getDestIndex(), request.getSourceIndex());
logger.debug("Creating destination index [{}] for source index [{}]", request.destIndex(), request.sourceIndex());

Settings settings = Settings.builder()
// add source settings
.put(filterSettings(sourceIndex))
// add override settings from request
.put(request.getSettingsOverride())
.put(request.settingsOverride())
.build();

Map<String, Object> mergeMappings;
try {
mergeMappings = mergeMappings(sourceIndex.mapping(), request.getMappingsOverride());
mergeMappings = mergeMappings(sourceIndex.mapping(), request.mappingsOverride());
} catch (IOException e) {
listener.onFailure(e);
return;
}

var createIndexRequest = new CreateIndexRequest(request.getDestIndex()).settings(settings);
var createIndexRequest = new CreateIndexRequest(request.destIndex()).settings(settings);
if (mergeMappings.isEmpty() == false) {
createIndexRequest.mapping(mergeMappings);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ private void createIndex(
Map.of()
);
request.setParentTask(parentTaskId);
var errorMessage = String.format(Locale.ROOT, "Could not create index [%s]", request.getDestIndex());
var errorMessage = String.format(Locale.ROOT, "Could not create index [%s]", request.destIndex());
client.execute(CreateIndexFromSourceAction.INSTANCE, request, failIfNotAcknowledged(listener, errorMessage));
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

package org.elasticsearch.xpack.migrate.rest;

import org.elasticsearch.client.internal.node.NodeClient;
import org.elasticsearch.rest.BaseRestHandler;
import org.elasticsearch.rest.RestRequest;
import org.elasticsearch.rest.action.RestToXContentListener;
import org.elasticsearch.xpack.migrate.action.CreateIndexFromSourceAction;

import java.io.IOException;
import java.util.List;

import static org.elasticsearch.rest.RestRequest.Method.POST;
import static org.elasticsearch.rest.RestRequest.Method.PUT;

public class RestCreateIndexFromSourceAction extends BaseRestHandler {

@Override
public String getName() {
return "create_index_from_source_action";
}

@Override
public List<Route> routes() {
return List.of(new Route(PUT, "/_create_from/{source}/{dest}"), new Route(POST, "/_create_from/{source}/{dest}"));
}

@Override
protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException {
var createRequest = new CreateIndexFromSourceAction.Request(request.param("source"), request.param("dest"));
request.applyContentParser(createRequest::fromXContent);
return channel -> client.execute(CreateIndexFromSourceAction.INSTANCE, createRequest, new RestToXContentListener<>(channel));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

package org.elasticsearch.xpack.migrate.action;

import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.io.stream.Writeable;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.test.AbstractWireSerializingTestCase;
import org.elasticsearch.test.hamcrest.ElasticsearchAssertions;
import org.elasticsearch.xcontent.XContentParser;
import org.elasticsearch.xcontent.XContentType;
import org.elasticsearch.xpack.migrate.action.CreateIndexFromSourceAction.Request;

import java.io.IOException;
import java.util.Map;

import static org.elasticsearch.xcontent.ToXContent.EMPTY_PARAMS;

public class CreateFromSourceIndexRequestTests extends AbstractWireSerializingTestCase<Request> {

public void testToAndFromXContent() throws IOException {
var request = createTestInstance();

boolean humanReadable = randomBoolean();
final XContentType xContentType = randomFrom(XContentType.values());
BytesReference originalBytes = toShuffledXContent(request, xContentType, EMPTY_PARAMS, humanReadable);

var parsedRequest = new Request(
randomValueOtherThan(request.sourceIndex(), () -> randomAlphanumericOfLength(30)),
randomValueOtherThan(request.sourceIndex(), () -> randomAlphanumericOfLength(30))
);
try (XContentParser xParser = createParser(xContentType.xContent(), originalBytes)) {
parsedRequest.fromXContent(xParser);
}

// source and dest won't be equal
assertNotEquals(request, parsedRequest);
assertNotEquals(request.sourceIndex(), parsedRequest.sourceIndex());
assertNotEquals(request.destIndex(), parsedRequest.destIndex());

// but fields in xcontent will be equal
assertEquals(request.settingsOverride(), parsedRequest.settingsOverride());
assertEquals(request.mappingsOverride(), parsedRequest.mappingsOverride());

BytesReference finalBytes = toShuffledXContent(parsedRequest, xContentType, EMPTY_PARAMS, humanReadable);
ElasticsearchAssertions.assertToXContentEquivalent(originalBytes, finalBytes, xContentType);
}

@Override
protected Writeable.Reader<Request> instanceReader() {
return Request::new;
}

@Override
protected Request createTestInstance() {
String source = randomAlphaOfLength(30);
String dest = randomAlphaOfLength(30);
if (randomBoolean()) {
return new Request(source, dest);
} else {
return new Request(source, dest, randomSettings(), randomMappings());
}
}

@Override
protected Request mutateInstance(Request instance) throws IOException {

String sourceIndex = instance.sourceIndex();
String destIndex = instance.destIndex();
Settings settingsOverride = instance.settingsOverride();
Map<String, Object> mappingsOverride = instance.mappingsOverride();

switch (between(0, 3)) {
case 0 -> sourceIndex = randomValueOtherThan(sourceIndex, () -> randomAlphaOfLength(30));
case 1 -> destIndex = randomValueOtherThan(destIndex, () -> randomAlphaOfLength(30));
case 2 -> settingsOverride = randomValueOtherThan(settingsOverride, CreateFromSourceIndexRequestTests::randomSettings);
case 3 -> mappingsOverride = randomValueOtherThan(mappingsOverride, CreateFromSourceIndexRequestTests::randomMappings);
}
return new Request(sourceIndex, destIndex, settingsOverride, mappingsOverride);
}

public static Map<String, Object> randomMappings() {
var randMappings = Map.of("properties", Map.of(randomAlphaOfLength(5), Map.of("type", "keyword")));
return randomBoolean() ? Map.of() : Map.of("_doc", randMappings);
}

public static Settings randomSettings() {
return randomBoolean() ? Settings.EMPTY : indexSettings(randomIntBetween(1, 10), randomIntBetween(0, 5)).build();
}
}
Loading

0 comments on commit b34e278

Please sign in to comment.