diff --git a/modules/data-streams/src/main/java/org/elasticsearch/datastreams/DataStreamsPlugin.java b/modules/data-streams/src/main/java/org/elasticsearch/datastreams/DataStreamsPlugin.java index cc75d3174e83c..b3802f1bbbb1c 100644 --- a/modules/data-streams/src/main/java/org/elasticsearch/datastreams/DataStreamsPlugin.java +++ b/modules/data-streams/src/main/java/org/elasticsearch/datastreams/DataStreamsPlugin.java @@ -70,10 +70,12 @@ import org.elasticsearch.datastreams.rest.RestCreateDataStreamAction; import org.elasticsearch.datastreams.rest.RestDataStreamsStatsAction; import org.elasticsearch.datastreams.rest.RestDeleteDataStreamAction; +import org.elasticsearch.datastreams.rest.RestGetDataStreamSettingsAction; import org.elasticsearch.datastreams.rest.RestGetDataStreamsAction; import org.elasticsearch.datastreams.rest.RestMigrateToDataStreamAction; import org.elasticsearch.datastreams.rest.RestModifyDataStreamsAction; import org.elasticsearch.datastreams.rest.RestPromoteDataStreamAction; +import org.elasticsearch.datastreams.rest.RestUpdateDataStreamSettingsAction; import org.elasticsearch.features.NodeFeature; import org.elasticsearch.health.HealthIndicatorService; import org.elasticsearch.index.IndexSettingProvider; @@ -280,6 +282,8 @@ public List getRestHandlers( handlers.add(new RestGetDataStreamOptionsAction()); handlers.add(new RestPutDataStreamOptionsAction()); handlers.add(new RestDeleteDataStreamOptionsAction()); + handlers.add(new RestGetDataStreamSettingsAction()); + handlers.add(new RestUpdateDataStreamSettingsAction()); return handlers; } diff --git a/modules/data-streams/src/main/java/org/elasticsearch/datastreams/rest/RestGetDataStreamSettingsAction.java b/modules/data-streams/src/main/java/org/elasticsearch/datastreams/rest/RestGetDataStreamSettingsAction.java new file mode 100644 index 0000000000000..dec7d2dd65bf1 --- /dev/null +++ b/modules/data-streams/src/main/java/org/elasticsearch/datastreams/rest/RestGetDataStreamSettingsAction.java @@ -0,0 +1,51 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +package org.elasticsearch.datastreams.rest; + +import org.elasticsearch.action.datastreams.GetDataStreamSettingsAction; +import org.elasticsearch.client.internal.node.NodeClient; +import org.elasticsearch.common.Strings; +import org.elasticsearch.rest.BaseRestHandler; +import org.elasticsearch.rest.RestRequest; +import org.elasticsearch.rest.RestUtils; +import org.elasticsearch.rest.Scope; +import org.elasticsearch.rest.ServerlessScope; +import org.elasticsearch.rest.action.RestCancellableNodeClient; +import org.elasticsearch.rest.action.RestRefCountedChunkedToXContentListener; + +import java.io.IOException; +import java.util.List; + +import static org.elasticsearch.rest.RestRequest.Method.GET; + +@ServerlessScope(Scope.PUBLIC) +public class RestGetDataStreamSettingsAction extends BaseRestHandler { + @Override + public String getName() { + return "gett_data_stream_settings_action"; + } + + @Override + public List routes() { + return List.of(new Route(GET, "/_data_stream/{name}/_settings")); + } + + @Override + protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { + GetDataStreamSettingsAction.Request getDataStreamRequest = new GetDataStreamSettingsAction.Request( + RestUtils.getMasterNodeTimeout(request) + ).indices(Strings.splitStringByCommaToArray(request.param("name"))); + return channel -> new RestCancellableNodeClient(client, request.getHttpChannel()).execute( + GetDataStreamSettingsAction.INSTANCE, + getDataStreamRequest, + new RestRefCountedChunkedToXContentListener<>(channel) + ); + } +} diff --git a/modules/data-streams/src/main/java/org/elasticsearch/datastreams/rest/RestUpdateDataStreamSettingsAction.java b/modules/data-streams/src/main/java/org/elasticsearch/datastreams/rest/RestUpdateDataStreamSettingsAction.java new file mode 100644 index 0000000000000..1b0a4b4637156 --- /dev/null +++ b/modules/data-streams/src/main/java/org/elasticsearch/datastreams/rest/RestUpdateDataStreamSettingsAction.java @@ -0,0 +1,59 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ +package org.elasticsearch.datastreams.rest; + +import org.elasticsearch.action.datastreams.UpdateDataStreamSettingsAction; +import org.elasticsearch.client.internal.node.NodeClient; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.rest.BaseRestHandler; +import org.elasticsearch.rest.RestRequest; +import org.elasticsearch.rest.RestUtils; +import org.elasticsearch.rest.Scope; +import org.elasticsearch.rest.ServerlessScope; +import org.elasticsearch.rest.action.RestCancellableNodeClient; +import org.elasticsearch.rest.action.RestRefCountedChunkedToXContentListener; +import org.elasticsearch.xcontent.XContentParser; + +import java.io.IOException; +import java.util.List; + +import static org.elasticsearch.rest.RestRequest.Method.PUT; + +@ServerlessScope(Scope.PUBLIC) +public class RestUpdateDataStreamSettingsAction extends BaseRestHandler { + + @Override + public String getName() { + return "update_data_stream_settings_action"; + } + + @Override + public List routes() { + return List.of(new Route(PUT, "/_data_stream/{name}/_settings")); + } + + @Override + protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { + Settings settings; + try (XContentParser parser = request.contentParser()) { + settings = Settings.fromXContent(parser); + } + UpdateDataStreamSettingsAction.Request putDataStreamRequest = new UpdateDataStreamSettingsAction.Request( + settings, + RestUtils.getMasterNodeTimeout(request), + RestUtils.getAckTimeout(request) + ).indices(Strings.splitStringByCommaToArray(request.param("name"))); + return channel -> new RestCancellableNodeClient(client, request.getHttpChannel()).execute( + UpdateDataStreamSettingsAction.INSTANCE, + putDataStreamRequest, + new RestRefCountedChunkedToXContentListener<>(channel) + ); + } +} diff --git a/modules/data-streams/src/yamlRestTest/resources/rest-api-spec/test/data_stream/240_data_stream_settings.yml b/modules/data-streams/src/yamlRestTest/resources/rest-api-spec/test/data_stream/240_data_stream_settings.yml new file mode 100644 index 0000000000000..13366a6c04685 --- /dev/null +++ b/modules/data-streams/src/yamlRestTest/resources/rest-api-spec/test/data_stream/240_data_stream_settings.yml @@ -0,0 +1,185 @@ +setup: + - skip: + features: allowed_warnings + +--- +"Test single data stream": + - do: + allowed_warnings: + - "index template [my-template] has index patterns [data-*] matching patterns from existing older templates [global] with patterns (global => [*]); this template [my-template] will take precedence during new index creation" + indices.put_index_template: + name: my-template + body: + index_patterns: [ my-data-stream-* ] + data_stream: { } + template: + settings: + number_of_replicas: 0 + lifecycle.name: my-policy + + - do: + indices.create_data_stream: + name: my-data-stream-1 + + - do: + cluster.health: + index: "my-data-stream-1" + wait_for_status: green + + - do: + indices.get_data_stream_settings: + name: my-data-stream-1 + - match: { data_streams.0.name: my-data-stream-1 } + - match: { data_streams.0.settings: {} } + - match: { data_streams.0.effective_settings.index.number_of_shards: null } + - match: { data_streams.0.effective_settings.index.number_of_replicas: "0" } + - match: { data_streams.0.effective_settings.index.lifecycle.name: "my-policy" } + + - do: + indices.get_data_stream: + name: my-data-stream-1 + - match: { data_streams.0.name: my-data-stream-1 } + - match: { data_streams.0.settings: {} } + - match: { data_streams.0.effective_settings: null } + + - do: + indices.put_data_stream_settings: + name: my-data-stream-1 + body: + index: + number_of_shards: 2 + lifecycle.name: my-new-policy + - match: { data_streams.0.name: my-data-stream-1 } + - match: { data_streams.0.applied_to_data_stream: true } + - match: { data_streams.0.index_settings_results.applied_to_data_stream_only: [index.number_of_shards]} + - match: { data_streams.0.index_settings_results.applied_to_data_stream_and_backing_indices: [index.lifecycle.name] } + - match: { data_streams.0.settings.index.number_of_shards: "2" } + - match: { data_streams.0.settings.index.lifecycle.name: "my-new-policy" } + - match: { data_streams.0.effective_settings.index.number_of_shards: "2" } + - match: { data_streams.0.effective_settings.index.number_of_replicas: "0" } + - match: { data_streams.0.effective_settings.index.lifecycle.name: "my-new-policy" } + + - do: + indices.rollover: + alias: "my-data-stream-1" + + - do: + cluster.health: + index: "my-data-stream-1" + wait_for_status: green + + - do: + indices.get_data_stream_settings: + name: my-data-stream-1 + - match: { data_streams.0.name: my-data-stream-1 } + - match: { data_streams.0.settings.index.number_of_shards: "2" } + - match: { data_streams.0.effective_settings.index.number_of_shards: "2" } + - match: { data_streams.0.effective_settings.index.number_of_replicas: "0" } + - match: { data_streams.0.effective_settings.index.lifecycle.name: "my-new-policy" } + + - do: + indices.get_data_stream: + name: my-data-stream-1 + - match: { data_streams.0.name: my-data-stream-1 } + - match: { data_streams.0.settings.index.number_of_shards: "2" } + - match: { data_streams.0.settings.index.lifecycle.name: "my-new-policy" } + - match: { data_streams.0.effective_settings: null } + + - do: + indices.get_data_stream: + name: my-data-stream-1 + - set: { data_streams.0.indices.0.index_name: idx0name } + - set: { data_streams.0.indices.1.index_name: idx1name } + + - do: + indices.get_settings: + index: my-data-stream-1 + - match: { .$idx0name.settings.index.number_of_shards: "1" } + - match: { .$idx0name.settings.index.lifecycle.name: "my-new-policy" } + - match: { .$idx1name.settings.index.number_of_shards: "2" } + - match: { .$idx1name.settings.index.lifecycle.name: "my-new-policy" } + +--- +"Test multiple data streams": + - do: + allowed_warnings: + - "index template [my-template] has index patterns [data-*] matching patterns from existing older templates [global] with patterns (global => [*]); this template [my-template] will take precedence during new index creation" + indices.put_index_template: + name: my-template + body: + index_patterns: [ my-data-stream-* ] + data_stream: { } + template: + settings: + number_of_replicas: 0 + lifecycle.name: my-policy + + - do: + indices.create_data_stream: + name: my-data-stream-1 + + - do: + indices.create_data_stream: + name: my-data-stream-2 + + - do: + indices.create_data_stream: + name: my-data-stream-3 + + - do: + cluster.health: + index: "my-data-stream-1,my-data-stream-2,my-data-stream-3" + wait_for_status: green + + - do: + indices.get_data_stream_settings: + name: "*" + - length: { data_streams: 3 } + + - do: + indices.get_data_stream_settings: + name: "my-data-stream-1,my-data-stream-2" + - length: { data_streams: 2 } + + - do: + indices.put_data_stream_settings: + name: my-data-stream-1,my-data-stream-2 + body: + index: + number_of_shards: 2 + lifecycle.name: my-new-policy + - length: { data_streams: 2 } + +--- +"Test invalid settings": + - do: + allowed_warnings: + - "index template [my-template] has index patterns [data-*] matching patterns from existing older templates [global] with patterns (global => [*]); this template [my-template] will take precedence during new index creation" + indices.put_index_template: + name: my-template + body: + index_patterns: [ my-data-stream-* ] + data_stream: { } + template: + settings: + number_of_replicas: 0 + lifecycle.name: my-policy + + - do: + indices.create_data_stream: + name: my-data-stream-1 + + - do: + cluster.health: + index: "my-data-stream-1" + wait_for_status: green + + - do: + indices.put_data_stream_settings: + name: my-data-stream-1 + body: + index: + fake_setting: 1234 + - match: { data_streams.0.name: my-data-stream-1 } + - match: { data_streams.0.applied_to_data_stream: false } + - match: { data_streams.0.error: "Cannot set the following settings on a data stream: [index.fake_setting]" } diff --git a/rest-api-spec/src/main/resources/rest-api-spec/api/indices.get_data_stream_settings.json b/rest-api-spec/src/main/resources/rest-api-spec/api/indices.get_data_stream_settings.json new file mode 100644 index 0000000000000..6bb597606a217 --- /dev/null +++ b/rest-api-spec/src/main/resources/rest-api-spec/api/indices.get_data_stream_settings.json @@ -0,0 +1,39 @@ +{ + "indices.get_data_stream_settings":{ + "documentation":{ + "url":"https://www.elastic.co/guide/en/elasticsearch/reference/master/data-streams.html", + "description":"Gets a data stream's settings" + }, + "stability":"stable", + "visibility":"public", + "headers":{ + "accept": [ "application/json"] + }, + "url":{ + "paths":[ + { + "path":"/_data_stream/{name}/_settings", + "methods":[ + "GET" + ], + "parts":{ + "name":{ + "type":"string", + "description":"The name of the data stream or data stream pattern" + } + } + } + ] + }, + "params":{ + "timeout":{ + "type":"time", + "description":"Specify timeout for acknowledging the cluster state update" + }, + "master_timeout":{ + "type":"time", + "description":"Specify timeout for connection to master" + } + } + } +} diff --git a/rest-api-spec/src/main/resources/rest-api-spec/api/indices.put_data_stream_settings.json b/rest-api-spec/src/main/resources/rest-api-spec/api/indices.put_data_stream_settings.json new file mode 100644 index 0000000000000..da873b320d5f4 --- /dev/null +++ b/rest-api-spec/src/main/resources/rest-api-spec/api/indices.put_data_stream_settings.json @@ -0,0 +1,43 @@ +{ + "indices.put_data_stream_settings":{ + "documentation":{ + "url":"https://www.elastic.co/guide/en/elasticsearch/reference/master/data-streams.html", + "description":"Updates a data stream's settings" + }, + "stability":"stable", + "visibility":"public", + "headers":{ + "accept": [ "application/json"] + }, + "url":{ + "paths":[ + { + "path":"/_data_stream/{name}/_settings", + "methods":[ + "PUT" + ], + "parts":{ + "name":{ + "type":"string", + "description":"The name of the data stream or data stream pattern" + } + } + } + ] + }, + "params":{ + "timeout":{ + "type":"time", + "description":"Specify timeout for acknowledging the cluster state update" + }, + "master_timeout":{ + "type":"time", + "description":"Specify timeout for connection to master" + } + }, + "body":{ + "description":"The data stream settings to be updated", + "required":true + } + } +} diff --git a/server/src/main/java/org/elasticsearch/action/datastreams/GetDataStreamAction.java b/server/src/main/java/org/elasticsearch/action/datastreams/GetDataStreamAction.java index 781d84bd4d98b..a9f5aac1b2e10 100644 --- a/server/src/main/java/org/elasticsearch/action/datastreams/GetDataStreamAction.java +++ b/server/src/main/java/org/elasticsearch/action/datastreams/GetDataStreamAction.java @@ -213,6 +213,7 @@ public static class DataStreamInfo implements SimpleDiffable, To public static final ParseField STATUS_FIELD = new ParseField("status"); public static final ParseField INDEX_TEMPLATE_FIELD = new ParseField("template"); + public static final ParseField SETTINGS_FIELD = new ParseField("settings"); public static final ParseField PREFER_ILM = new ParseField("prefer_ilm"); public static final ParseField MANAGED_BY = new ParseField("managed_by"); public static final ParseField NEXT_GENERATION_INDEX_MANAGED_BY = new ParseField("next_generation_managed_by"); @@ -417,7 +418,9 @@ public XContentBuilder toXContent( builder.endArray(); builder.endObject(); } - + builder.startObject(SETTINGS_FIELD.getPreferredName()); + dataStream.getSettings().toXContent(builder, params); + builder.endObject(); builder.startObject(DataStream.FAILURE_STORE_FIELD.getPreferredName()); builder.field(FAILURE_STORE_ENABLED.getPreferredName(), failureStoreEffectivelyEnabled); builder.field(DataStream.ROLLOVER_ON_WRITE_FIELD.getPreferredName(), dataStream.getFailureComponent().isRolloverOnWrite()); diff --git a/server/src/test/java/org/elasticsearch/action/datastreams/GetDataStreamActionTests.java b/server/src/test/java/org/elasticsearch/action/datastreams/GetDataStreamActionTests.java index 0614356943d3a..9aa1a2f2a2c07 100644 --- a/server/src/test/java/org/elasticsearch/action/datastreams/GetDataStreamActionTests.java +++ b/server/src/test/java/org/elasticsearch/action/datastreams/GetDataStreamActionTests.java @@ -15,6 +15,7 @@ import org.elasticsearch.cluster.metadata.DataStreamGlobalRetention; import org.elasticsearch.cluster.metadata.DataStreamLifecycle; import org.elasticsearch.common.Strings; +import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.core.TimeValue; import org.elasticsearch.index.Index; @@ -27,6 +28,7 @@ import java.util.List; import java.util.Map; +import static org.elasticsearch.cluster.metadata.ComponentTemplateTests.randomSettings; import static org.hamcrest.Matchers.equalTo; public class GetDataStreamActionTests extends ESTestCase { @@ -60,6 +62,8 @@ public void testDataStreamInfoToXContent() throws IOException { assertThat(lifecycleResult.get("data_retention"), equalTo(configuredRetention.getStringRep())); assertThat(lifecycleResult.get("effective_retention"), equalTo(globalMaxRetention.getStringRep())); assertThat(lifecycleResult.get("retention_determined_by"), equalTo("max_global_retention")); + Map> settingsMap = (Map>) resultMap.get("settings"); + assertThat(Settings.builder().loadFromMap(settingsMap).build(), equalTo(dataStreamInfo.getDataStream().getSettings())); } } @@ -100,6 +104,7 @@ private static GetDataStreamAction.Response.DataStreamInfo newDataStreamInfo(boo private static DataStream newDataStreamInstance(boolean isSystem, TimeValue retention) { List indices = List.of(new Index(randomAlphaOfLength(10), randomAlphaOfLength(10))); DataStreamLifecycle lifecycle = DataStreamLifecycle.createDataLifecycle(true, retention, null); + Settings settings = randomSettings(); return DataStream.builder(randomAlphaOfLength(50), indices) .setGeneration(randomLongBetween(1, 1000)) .setMetadata(Map.of()) @@ -107,6 +112,7 @@ private static DataStream newDataStreamInstance(boolean isSystem, TimeValue rete .setHidden(isSystem) .setReplicated(randomBoolean()) .setLifecycle(lifecycle) + .setSettings(settings) .build(); } }