Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add rest endpoint for create_from_source_index #119250

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 {
parkertimmins marked this conversation as resolved.
Show resolved Hide resolved
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