From 74dc598e5d92c42aa526dc75683e7d65ecc07420 Mon Sep 17 00:00:00 2001 From: Nikola Grcevski Date: Thu, 21 Apr 2022 12:42:16 -0400 Subject: [PATCH 01/48] WIP. --- .../elasticsearch/action/ActionModule.java | 16 +++++ .../TransportClusterUpdateSettingsAction.java | 53 ++++++++++----- .../java/org/elasticsearch/node/Node.java | 2 + .../operator/OperatorHandler.java | 51 ++++++++++++++ .../operator/OperatorSettingsController.java | 56 ++++++++++++++++ .../action/OperatorClusterSettingsAction.java | 59 ++++++++++++++++ .../elasticsearch/plugins/ActionPlugin.java | 5 ++ .../RestClusterUpdateSettingsAction.java | 4 +- .../OperatorSettingsControllerTests.java | 43 ++++++++++++ .../xpack/ilm/IndexLifecycle.java | 7 ++ .../ilm/action/RestPutLifecycleAction.java | 4 +- .../operator/OperatorLifecycleAction.java | 67 +++++++++++++++++++ 12 files changed, 347 insertions(+), 20 deletions(-) create mode 100644 server/src/main/java/org/elasticsearch/operator/OperatorHandler.java create mode 100644 server/src/main/java/org/elasticsearch/operator/OperatorSettingsController.java create mode 100644 server/src/main/java/org/elasticsearch/operator/action/OperatorClusterSettingsAction.java create mode 100644 test/framework/src/test/java/org/elasticsearch/operator/OperatorSettingsControllerTests.java create mode 100644 x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/operator/OperatorLifecycleAction.java diff --git a/server/src/main/java/org/elasticsearch/action/ActionModule.java b/server/src/main/java/org/elasticsearch/action/ActionModule.java index 5329c56ba19d3..9d4594e2ddbff 100644 --- a/server/src/main/java/org/elasticsearch/action/ActionModule.java +++ b/server/src/main/java/org/elasticsearch/action/ActionModule.java @@ -262,6 +262,9 @@ import org.elasticsearch.indices.SystemIndices; import org.elasticsearch.indices.breaker.CircuitBreakerService; import org.elasticsearch.indices.store.TransportNodesListShardStoreMetadata; +import org.elasticsearch.operator.OperatorHandler; +import org.elasticsearch.operator.OperatorSettingsController; +import org.elasticsearch.operator.action.OperatorClusterSettingsAction; import org.elasticsearch.persistent.CompletionPersistentTaskAction; import org.elasticsearch.persistent.RemovePersistentTaskAction; import org.elasticsearch.persistent.StartPersistentTaskAction; @@ -441,6 +444,7 @@ public class ActionModule extends AbstractModule { private final RequestValidators mappingRequestValidators; private final RequestValidators indicesAliasesRequestRequestValidators; private final ThreadPool threadPool; + private final OperatorSettingsController operatorController; public ActionModule( Settings settings, @@ -502,6 +506,7 @@ public ActionModule( ); restController = new RestController(headers, restWrapper, nodeClient, circuitBreakerService, usageService); + operatorController = new OperatorSettingsController(); } public Map> getActions() { @@ -874,6 +879,17 @@ public void initRestHandlers(Supplier nodesInCluster) { registerHandler.accept(new RestCatAction(catActions)); } + public void initOperatorHandlers() { + List handlers = new ArrayList<>(); + + handlers.add(new OperatorClusterSettingsAction()); + for (ActionPlugin plugin : actionPlugins) { + handlers.addAll(plugin.getOperatorHandlers()); + } + + operatorController.initHandlers(handlers); + } + @Override protected void configure() { bind(ActionFilters.class).toInstance(actionFilters); diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/settings/TransportClusterUpdateSettingsAction.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/settings/TransportClusterUpdateSettingsAction.java index b2c817745abbb..0fef5f60f6c42 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/cluster/settings/TransportClusterUpdateSettingsAction.java +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/settings/TransportClusterUpdateSettingsAction.java @@ -14,11 +14,13 @@ import org.elasticsearch.ElasticsearchException; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.support.ActionFilters; +import org.elasticsearch.action.support.master.AcknowledgedResponse; import org.elasticsearch.action.support.master.TransportMasterNodeAction; import org.elasticsearch.cluster.AckedClusterStateUpdateTask; import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.ClusterStateTaskExecutor; import org.elasticsearch.cluster.ClusterStateUpdateTask; +import org.elasticsearch.cluster.ack.AckedRequest; import org.elasticsearch.cluster.block.ClusterBlockException; import org.elasticsearch.cluster.block.ClusterBlockLevel; import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; @@ -144,11 +146,12 @@ protected void masterOperation( final ClusterState state, final ActionListener listener ) { - final SettingsUpdater updater = new SettingsUpdater(clusterSettings); - clusterService.submitStateUpdateTask(UPDATE_TASK_SOURCE, new AckedClusterStateUpdateTask(Priority.IMMEDIATE, request, listener) { - - private volatile boolean changed = false; - + clusterService.submitStateUpdateTask(UPDATE_TASK_SOURCE, new ClusterUpdateSettingsTask( + clusterSettings, + Priority.IMMEDIATE, + request, + listener + ) { @Override protected ClusterUpdateSettingsResponse newResponse(boolean acknowledged) { return new ClusterUpdateSettingsResponse(acknowledged, updater.getTransientUpdates(), updater.getPersistentUpdate()); @@ -254,21 +257,37 @@ public void onFailure(Exception e) { logger.debug(() -> new ParameterizedMessage("failed to perform [{}]", UPDATE_TASK_SOURCE), e); super.onFailure(e); } - - @Override - public ClusterState execute(final ClusterState currentState) { - final ClusterState clusterState = updater.updateSettings( - currentState, - clusterSettings.upgradeSettings(request.transientSettings()), - clusterSettings.upgradeSettings(request.persistentSettings()), - logger - ); - changed = clusterState != currentState; - return clusterState; - } }, newExecutor()); } + public class ClusterUpdateSettingsTask extends AckedClusterStateUpdateTask { + protected volatile boolean changed = false; + protected final SettingsUpdater updater; + protected final ClusterUpdateSettingsRequest request; + + public ClusterUpdateSettingsTask( + final ClusterSettings clusterSettings, + Priority priority, + ClusterUpdateSettingsRequest request, + ActionListener listener) { + super(priority, request, listener); + this.updater = new SettingsUpdater(clusterSettings); + this.request = request; + } + + @Override + public ClusterState execute(final ClusterState currentState) { + final ClusterState clusterState = updater.updateSettings( + currentState, + clusterSettings.upgradeSettings(request.transientSettings()), + clusterSettings.upgradeSettings(request.persistentSettings()), + logger + ); + changed = clusterState != currentState; + return clusterState; + } + } + @SuppressForbidden(reason = "legacy usage of unbatched task") // TODO add support for batching here private static ClusterStateTaskExecutor newExecutor() { return ClusterStateTaskExecutor.unbatched(); diff --git a/server/src/main/java/org/elasticsearch/node/Node.java b/server/src/main/java/org/elasticsearch/node/Node.java index 88760a061da77..cb8c11f82f921 100644 --- a/server/src/main/java/org/elasticsearch/node/Node.java +++ b/server/src/main/java/org/elasticsearch/node/Node.java @@ -1026,6 +1026,8 @@ protected Node( logger.debug("initializing HTTP handlers ..."); actionModule.initRestHandlers(() -> clusterService.state().nodes()); + logger.debug("initializing operator handlers ..."); + actionModule.initOperatorHandlers(); logger.info("initialized"); success = true; diff --git a/server/src/main/java/org/elasticsearch/operator/OperatorHandler.java b/server/src/main/java/org/elasticsearch/operator/OperatorHandler.java new file mode 100644 index 0000000000000..230eb955b6172 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/operator/OperatorHandler.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 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 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.operator; + +import org.elasticsearch.ElasticsearchGenerationException; +import org.elasticsearch.action.support.master.MasterNodeRequest; +import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.common.Strings; +import org.elasticsearch.xcontent.XContentBuilder; +import org.elasticsearch.xcontent.XContentFactory; +import org.elasticsearch.xcontent.XContentParser; +import org.elasticsearch.xcontent.XContentParserConfiguration; +import org.elasticsearch.xcontent.XContentType; + +import java.io.IOException; +import java.util.Collection; +import java.util.Map; +import java.util.Optional; + +/** + * TODO: Add docs + */ +public interface OperatorHandler { + String CONTENT = "content"; + + String key(); + + Collection prepare(Object source) throws IOException; + + Optional transformClusterState( + Collection requests, + ClusterState.Builder clusterStateBuilder, + ClusterState previous + ); + + default XContentParser mapToXContentParser(Map source) { + try (XContentBuilder builder = XContentFactory.contentBuilder(XContentType.JSON)) { + builder.map(source); + return XContentFactory.xContent(builder.contentType()) + .createParser(XContentParserConfiguration.EMPTY, Strings.toString(builder)); + } catch (IOException e) { + throw new ElasticsearchGenerationException("Failed to generate [" + source + "]", e); + } + } +} diff --git a/server/src/main/java/org/elasticsearch/operator/OperatorSettingsController.java b/server/src/main/java/org/elasticsearch/operator/OperatorSettingsController.java new file mode 100644 index 0000000000000..459b753ddb4b5 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/operator/OperatorSettingsController.java @@ -0,0 +1,56 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.operator; + +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.action.support.master.MasterNodeRequest; +import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.xcontent.XContentParser; + +import java.io.IOException; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * TODO: Write docs + */ +public class OperatorSettingsController { + Map handlers = null; + + public void initHandlers(List handlerList) { + handlers = handlerList.stream().collect(Collectors.toMap(OperatorHandler::key, Function.identity())); + } + + public ClusterState process(XContentParser parser) throws IOException { + Map source = parser.map(); + + source.keySet().forEach(k -> { + OperatorHandler handler = handlers.get(k); + if (handler == null) { + throw new IllegalStateException("Unknown entity type " + k); + } + try { + Collection> requests = handler.prepare(source.get(k)); + for (MasterNodeRequest request : requests) { + ActionRequestValidationException exception = request.validate(); + if (exception != null) { + throw new IllegalStateException("Validation error", exception); + } + } + } catch (IOException e) { + throw new IllegalStateException("Malformed input data", e); + } + }); + + return null; + } +} diff --git a/server/src/main/java/org/elasticsearch/operator/action/OperatorClusterSettingsAction.java b/server/src/main/java/org/elasticsearch/operator/action/OperatorClusterSettingsAction.java new file mode 100644 index 0000000000000..b033d4777e629 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/operator/action/OperatorClusterSettingsAction.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 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 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.operator.action; + +import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsRequest; +import org.elasticsearch.action.support.master.MasterNodeRequest; +import org.elasticsearch.client.internal.Requests; +import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.operator.OperatorHandler; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static org.elasticsearch.rest.action.admin.cluster.RestClusterUpdateSettingsAction.PERSISTENT; +import static org.elasticsearch.rest.action.admin.cluster.RestClusterUpdateSettingsAction.TRANSIENT; + +/** + * TODO: Add docs + */ +public class OperatorClusterSettingsAction implements OperatorHandler { + + public static final String KEY = "cluster"; + + @Override + public String key() { + return KEY; + } + + @Override + @SuppressWarnings("unchecked") + public Collection prepare(Object input) { + if (input instanceof Map source) { + final ClusterUpdateSettingsRequest clusterUpdateSettingsRequest = Requests.clusterUpdateSettingsRequest(); + + if (source.containsKey(PERSISTENT)) { + clusterUpdateSettingsRequest.persistentSettings((Map) source.get(PERSISTENT)); + } + + return List.of(clusterUpdateSettingsRequest); + } + throw new IllegalStateException("Unsupported " + KEY + " request format"); + } + + @Override + public Optional transformClusterState( + Collection requests, + ClusterState.Builder clusterStateBuilder, + ClusterState previous) { + return Optional.empty(); + } +} diff --git a/server/src/main/java/org/elasticsearch/plugins/ActionPlugin.java b/server/src/main/java/org/elasticsearch/plugins/ActionPlugin.java index 3163d02b27e9b..fe913bf2071f7 100644 --- a/server/src/main/java/org/elasticsearch/plugins/ActionPlugin.java +++ b/server/src/main/java/org/elasticsearch/plugins/ActionPlugin.java @@ -23,6 +23,7 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.settings.SettingsFilter; import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.operator.OperatorHandler; import org.elasticsearch.rest.RestController; import org.elasticsearch.rest.RestHandler; import org.elasticsearch.rest.RestHeaderDefinition; @@ -84,6 +85,10 @@ default List getRestHandlers( return Collections.emptyList(); } + default List getOperatorHandlers() { + return Collections.emptyList(); + } + /** * Returns headers which should be copied through rest requests on to internal requests. */ diff --git a/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestClusterUpdateSettingsAction.java b/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestClusterUpdateSettingsAction.java index 21ad55b4684ec..d2cb9b320a671 100644 --- a/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestClusterUpdateSettingsAction.java +++ b/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestClusterUpdateSettingsAction.java @@ -25,8 +25,8 @@ import static org.elasticsearch.rest.RestRequest.Method.PUT; public class RestClusterUpdateSettingsAction extends BaseRestHandler { - private static final String PERSISTENT = "persistent"; - private static final String TRANSIENT = "transient"; + public static final String PERSISTENT = "persistent"; + public static final String TRANSIENT = "transient"; @Override public List routes() { diff --git a/test/framework/src/test/java/org/elasticsearch/operator/OperatorSettingsControllerTests.java b/test/framework/src/test/java/org/elasticsearch/operator/OperatorSettingsControllerTests.java new file mode 100644 index 0000000000000..432f48ff87b75 --- /dev/null +++ b/test/framework/src/test/java/org/elasticsearch/operator/OperatorSettingsControllerTests.java @@ -0,0 +1,43 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.operator; + +import org.elasticsearch.operator.action.OperatorClusterSettingsAction; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xcontent.XContentParser; +import org.elasticsearch.xcontent.XContentParserConfiguration; +import org.elasticsearch.xcontent.XContentType; + +import java.io.IOException; +import java.util.List; + +public class OperatorSettingsControllerTests extends ESTestCase { + + public void testOperatorController() throws IOException { + OperatorSettingsController controller = new OperatorSettingsController(); + controller.initHandlers(List.of(new OperatorClusterSettingsAction())); + + String testJSON = """ + { + "cluster" : { + "persistent" : { + "indices.recovery.max_bytes_per_sec" : "50mb" + }, + "transient" : { + "cluster.routing.allocation.enable": "none" + } + } + } + """; + + try (XContentParser parser = XContentType.JSON.xContent().createParser(XContentParserConfiguration.EMPTY, testJSON)) { + controller.process(parser); + } + } +} diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycle.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycle.java index b2d943207f16c..16d69af053358 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycle.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycle.java @@ -29,6 +29,7 @@ import org.elasticsearch.health.HealthIndicatorService; import org.elasticsearch.index.IndexModule; import org.elasticsearch.license.XPackLicenseState; +import org.elasticsearch.operator.OperatorHandler; import org.elasticsearch.plugins.ActionPlugin; import org.elasticsearch.plugins.HealthPlugin; import org.elasticsearch.plugins.Plugin; @@ -105,6 +106,7 @@ import org.elasticsearch.xpack.ilm.action.TransportRetryAction; import org.elasticsearch.xpack.ilm.action.TransportStartILMAction; import org.elasticsearch.xpack.ilm.action.TransportStopILMAction; +import org.elasticsearch.xpack.ilm.action.operator.OperatorLifecycleAction; import org.elasticsearch.xpack.ilm.history.ILMHistoryStore; import org.elasticsearch.xpack.ilm.history.ILMHistoryTemplateRegistry; import org.elasticsearch.xpack.slm.SLMInfoTransportAction; @@ -378,6 +380,11 @@ public List getRestHandlers( return handlers; } + @Override + public List getOperatorHandlers() { + return List.of(new OperatorLifecycleAction()); + } + @Override public List> getActions() { var ilmUsageAction = new ActionHandler<>(XPackUsageFeatureAction.INDEX_LIFECYCLE, IndexLifecycleUsageTransportAction.class); diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/RestPutLifecycleAction.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/RestPutLifecycleAction.java index f15a244710987..06a3fd5223c16 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/RestPutLifecycleAction.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/RestPutLifecycleAction.java @@ -21,6 +21,8 @@ public class RestPutLifecycleAction extends BaseRestHandler { + public static final String NAME = "name"; + @Override public List routes() { return List.of(new Route(PUT, "/_ilm/policy/{name}")); @@ -33,7 +35,7 @@ public String getName() { @Override protected RestChannelConsumer prepareRequest(RestRequest restRequest, NodeClient client) throws IOException { - String lifecycleName = restRequest.param("name"); + String lifecycleName = restRequest.param(NAME); try (XContentParser parser = restRequest.contentParser()) { PutLifecycleAction.Request putLifecycleRequest = PutLifecycleAction.Request.parseRequest(lifecycleName, parser); putLifecycleRequest.timeout(restRequest.paramAsTime("timeout", putLifecycleRequest.timeout())); diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/operator/OperatorLifecycleAction.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/operator/OperatorLifecycleAction.java new file mode 100644 index 0000000000000..bd28469f0ac18 --- /dev/null +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/operator/OperatorLifecycleAction.java @@ -0,0 +1,67 @@ +/* + * 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.ilm.action.operator; + +import org.elasticsearch.action.support.master.MasterNodeRequest; +import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.operator.OperatorHandler; +import org.elasticsearch.xcontent.XContentParser; +import org.elasticsearch.xpack.core.ilm.action.PutLifecycleAction; +import org.elasticsearch.xpack.ilm.action.RestPutLifecycleAction; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** + * TODO: Add docs + */ +public class OperatorLifecycleAction extends OperatorHandler { + + public static final String KEY = "ilm"; + + @Override + public String key() { + return KEY; + } + + @Override + @SuppressWarnings("unchecked") + public Collection> prepare(Object source) throws IOException { + List> result = new ArrayList<>(); + + if (source.getClass().isArray()) { + Map[] sources = (Map[]) source; + for (Map s : sources) { + result.add(prepare(s)); + } + } else { + result.add(prepare((Map) source)); + } + + return result; + } + + @SuppressWarnings("unchecked") + private MasterNodeRequest prepare(Map source) throws IOException { + String lifecycleName = (String) source.get(RestPutLifecycleAction.NAME); + Map content = (Map) source.get(OperatorHandler.CONTENT); + + try (XContentParser parser = mapToXContentParser(content)) { + return PutLifecycleAction.Request.parseRequest(lifecycleName, parser); + } + } + + @Override + public Optional transformClusterState(Collection> requests, ClusterState previous) { + return Optional.empty(); + } +} From 44fc2d08642ebeb6a3537cb09892db8ed20025cd Mon Sep 17 00:00:00 2001 From: Nikola Grcevski Date: Thu, 21 Apr 2022 12:44:48 -0400 Subject: [PATCH 02/48] Fix compile error. --- .../action/operator/OperatorLifecycleAction.java | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/operator/OperatorLifecycleAction.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/operator/OperatorLifecycleAction.java index bd28469f0ac18..3028f5d59e019 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/operator/OperatorLifecycleAction.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/operator/OperatorLifecycleAction.java @@ -7,7 +7,6 @@ package org.elasticsearch.xpack.ilm.action.operator; -import org.elasticsearch.action.support.master.MasterNodeRequest; import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.operator.OperatorHandler; import org.elasticsearch.xcontent.XContentParser; @@ -24,7 +23,7 @@ /** * TODO: Add docs */ -public class OperatorLifecycleAction extends OperatorHandler { +public class OperatorLifecycleAction implements OperatorHandler { public static final String KEY = "ilm"; @@ -35,8 +34,8 @@ public String key() { @Override @SuppressWarnings("unchecked") - public Collection> prepare(Object source) throws IOException { - List> result = new ArrayList<>(); + public Collection prepare(Object source) throws IOException { + List result = new ArrayList<>(); if (source.getClass().isArray()) { Map[] sources = (Map[]) source; @@ -51,7 +50,7 @@ public Collection> prepare(Object source) throws IOExceptio } @SuppressWarnings("unchecked") - private MasterNodeRequest prepare(Map source) throws IOException { + private PutLifecycleAction.Request prepare(Map source) throws IOException { String lifecycleName = (String) source.get(RestPutLifecycleAction.NAME); Map content = (Map) source.get(OperatorHandler.CONTENT); @@ -61,7 +60,10 @@ private MasterNodeRequest prepare(Map source) throws IOException { } @Override - public Optional transformClusterState(Collection> requests, ClusterState previous) { + public Optional transformClusterState( + Collection requests, + ClusterState.Builder clusterStateBuilder, + ClusterState previous) { return Optional.empty(); } } From 9244455baa60eaff266a8ea9714fca1181c9a302 Mon Sep 17 00:00:00 2001 From: Nikola Grcevski Date: Mon, 25 Apr 2022 13:47:55 -0400 Subject: [PATCH 03/48] More WIP --- .../elasticsearch/action/ActionModule.java | 8 +- .../TransportClusterUpdateSettingsAction.java | 10 ++- .../java/org/elasticsearch/node/Node.java | 3 +- .../operator/OperatorHandler.java | 27 ++++-- .../operator/OperatorSettingsController.java | 81 ++++++++++++++---- .../action/OperatorClusterSettingsAction.java | 28 ++++--- .../OperatorSettingsControllerTests.java | 82 +++++++++++++++++++ .../OperatorSettingsControllerTests.java | 43 ---------- .../operator/OperatorLifecycleAction.java | 3 +- 9 files changed, 198 insertions(+), 87 deletions(-) create mode 100644 server/src/test/java/org/elasticsearch/operator/OperatorSettingsControllerTests.java delete mode 100644 test/framework/src/test/java/org/elasticsearch/operator/OperatorSettingsControllerTests.java diff --git a/server/src/main/java/org/elasticsearch/action/ActionModule.java b/server/src/main/java/org/elasticsearch/action/ActionModule.java index 9d4594e2ddbff..ac2b95e35017e 100644 --- a/server/src/main/java/org/elasticsearch/action/ActionModule.java +++ b/server/src/main/java/org/elasticsearch/action/ActionModule.java @@ -246,6 +246,7 @@ import org.elasticsearch.client.internal.node.NodeClient; import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; import org.elasticsearch.cluster.node.DiscoveryNodes; +import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.NamedRegistry; import org.elasticsearch.common.inject.AbstractModule; import org.elasticsearch.common.inject.TypeLiteral; @@ -445,6 +446,7 @@ public class ActionModule extends AbstractModule { private final RequestValidators indicesAliasesRequestRequestValidators; private final ThreadPool threadPool; private final OperatorSettingsController operatorController; + private final ClusterService clusterService; public ActionModule( Settings settings, @@ -457,7 +459,8 @@ public ActionModule( NodeClient nodeClient, CircuitBreakerService circuitBreakerService, UsageService usageService, - SystemIndices systemIndices + SystemIndices systemIndices, + ClusterService clusterService ) { this.settings = settings; this.indexNameExpressionResolver = indexNameExpressionResolver; @@ -466,6 +469,7 @@ public ActionModule( this.settingsFilter = settingsFilter; this.actionPlugins = actionPlugins; this.threadPool = threadPool; + this.clusterService = clusterService; actions = setupActions(actionPlugins); actionFilters = setupActionFilters(actionPlugins); autoCreateIndex = new AutoCreateIndex(settings, clusterSettings, indexNameExpressionResolver, systemIndices); @@ -506,7 +510,7 @@ public ActionModule( ); restController = new RestController(headers, restWrapper, nodeClient, circuitBreakerService, usageService); - operatorController = new OperatorSettingsController(); + operatorController = new OperatorSettingsController(clusterSettings, clusterService); } public Map> getActions() { diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/settings/TransportClusterUpdateSettingsAction.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/settings/TransportClusterUpdateSettingsAction.java index 0fef5f60f6c42..a5753732d51c5 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/cluster/settings/TransportClusterUpdateSettingsAction.java +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/settings/TransportClusterUpdateSettingsAction.java @@ -260,10 +260,11 @@ public void onFailure(Exception e) { }, newExecutor()); } - public class ClusterUpdateSettingsTask extends AckedClusterStateUpdateTask { + public static class ClusterUpdateSettingsTask extends AckedClusterStateUpdateTask { protected volatile boolean changed = false; protected final SettingsUpdater updater; protected final ClusterUpdateSettingsRequest request; + private final ClusterSettings clusterSettings; public ClusterUpdateSettingsTask( final ClusterSettings clusterSettings, @@ -271,10 +272,17 @@ public ClusterUpdateSettingsTask( ClusterUpdateSettingsRequest request, ActionListener listener) { super(priority, request, listener); + this.clusterSettings = clusterSettings; this.updater = new SettingsUpdater(clusterSettings); this.request = request; } + public ClusterUpdateSettingsTask( + final ClusterSettings clusterSettings, + ClusterUpdateSettingsRequest request) { + this(clusterSettings, Priority.IMMEDIATE, request, null); + } + @Override public ClusterState execute(final ClusterState currentState) { final ClusterState clusterState = updater.updateSettings( diff --git a/server/src/main/java/org/elasticsearch/node/Node.java b/server/src/main/java/org/elasticsearch/node/Node.java index cb8c11f82f921..d87c9eeb69ed4 100644 --- a/server/src/main/java/org/elasticsearch/node/Node.java +++ b/server/src/main/java/org/elasticsearch/node/Node.java @@ -700,7 +700,8 @@ protected Node( client, circuitBreakerService, usageService, - systemIndices + systemIndices, + clusterService ); modules.add(actionModule); diff --git a/server/src/main/java/org/elasticsearch/operator/OperatorHandler.java b/server/src/main/java/org/elasticsearch/operator/OperatorHandler.java index 230eb955b6172..be37451a373d7 100644 --- a/server/src/main/java/org/elasticsearch/operator/OperatorHandler.java +++ b/server/src/main/java/org/elasticsearch/operator/OperatorHandler.java @@ -9,9 +9,11 @@ package org.elasticsearch.operator; import org.elasticsearch.ElasticsearchGenerationException; +import org.elasticsearch.action.ActionRequestValidationException; import org.elasticsearch.action.support.master.MasterNodeRequest; import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.common.Strings; +import org.elasticsearch.common.settings.ClusterSettings; import org.elasticsearch.xcontent.XContentBuilder; import org.elasticsearch.xcontent.XContentFactory; import org.elasticsearch.xcontent.XContentParser; @@ -20,25 +22,34 @@ import java.io.IOException; import java.util.Collection; +import java.util.Collections; import java.util.Map; -import java.util.Optional; /** * TODO: Add docs */ -public interface OperatorHandler { +public interface OperatorHandler { String CONTENT = "content"; String key(); - Collection prepare(Object source) throws IOException; - - Optional transformClusterState( - Collection requests, - ClusterState.Builder clusterStateBuilder, - ClusterState previous + ClusterState transform( + Object source, + ClusterSettings clusterSettings, + ClusterState state ); + default Collection dependencies() { + return Collections.emptyList(); + } + + default void validate(T request) { + ActionRequestValidationException exception = request.validate(); + if (exception != null) { + throw new IllegalStateException("Validation error", exception); + } + } + default XContentParser mapToXContentParser(Map source) { try (XContentBuilder builder = XContentFactory.contentBuilder(XContentType.JSON)) { builder.map(source); diff --git a/server/src/main/java/org/elasticsearch/operator/OperatorSettingsController.java b/server/src/main/java/org/elasticsearch/operator/OperatorSettingsController.java index 459b753ddb4b5..c3bf644c35cd3 100644 --- a/server/src/main/java/org/elasticsearch/operator/OperatorSettingsController.java +++ b/server/src/main/java/org/elasticsearch/operator/OperatorSettingsController.java @@ -8,15 +8,17 @@ package org.elasticsearch.operator; -import org.elasticsearch.action.ActionRequestValidationException; -import org.elasticsearch.action.support.master.MasterNodeRequest; import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.common.settings.ClusterSettings; import org.elasticsearch.xcontent.XContentParser; import java.io.IOException; -import java.util.Collection; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; import java.util.stream.Collectors; @@ -25,6 +27,13 @@ */ public class OperatorSettingsController { Map handlers = null; + final ClusterSettings clusterSettings; + final ClusterService clusterService; + + public OperatorSettingsController(ClusterSettings clusterSettings, ClusterService clusterService) { + this.clusterSettings = clusterSettings; + this.clusterService = clusterService; + } public void initHandlers(List handlerList) { handlers = handlerList.stream().collect(Collectors.toMap(OperatorHandler::key, Function.identity())); @@ -33,24 +42,60 @@ public void initHandlers(List handlerList) { public ClusterState process(XContentParser parser) throws IOException { Map source = parser.map(); - source.keySet().forEach(k -> { + LinkedHashSet orderedHandler = orderedStateHandlers(source.keySet()); + + AtomicReference state = new AtomicReference<>(clusterService.state()); + + orderedHandler.forEach(k -> { OperatorHandler handler = handlers.get(k); - if (handler == null) { - throw new IllegalStateException("Unknown entity type " + k); - } - try { - Collection> requests = handler.prepare(source.get(k)); - for (MasterNodeRequest request : requests) { - ActionRequestValidationException exception = request.validate(); - if (exception != null) { - throw new IllegalStateException("Validation error", exception); - } - } - } catch (IOException e) { - throw new IllegalStateException("Malformed input data", e); - } + state.set(handler.transform(source.get(k), clusterSettings, state.get())); }); return null; } + + LinkedHashSet orderedStateHandlers(Set keys) { + LinkedHashSet orderedHandlers = new LinkedHashSet<>(); + LinkedHashSet dependencyStack = new LinkedHashSet<>(); + + for (String key : keys) { + addStateHandler(key, keys, orderedHandlers, dependencyStack); + } + + return orderedHandlers; + } + + void addStateHandler(String key, Set keys, LinkedHashSet ordered, LinkedHashSet visited) { + if (visited.contains(key)) { + StringBuilder msg = new StringBuilder("Cycle found in settings dependencies: "); + visited.forEach(s -> { + msg.append(s); + msg.append(" -> "); + }); + msg.append(key); + throw new IllegalStateException(msg.toString()); + } + + if (ordered.contains(key)) { + // already added by another dependent handler + return; + } + + visited.add(key); + OperatorHandler handler = handlers.get(key); + + if (handler == null) { + throw new IllegalStateException("Unknown settings definition type: " + key); + } + + for (String dependency : handler.dependencies()) { + if (keys.contains(dependency) == false) { + throw new IllegalStateException("Missing settings dependency definition: " + key + " -> " + dependency); + } + addStateHandler(dependency, keys, ordered, visited); + } + + visited.remove(key); + ordered.add(key); + } } diff --git a/server/src/main/java/org/elasticsearch/operator/action/OperatorClusterSettingsAction.java b/server/src/main/java/org/elasticsearch/operator/action/OperatorClusterSettingsAction.java index b033d4777e629..30a9f2dc6e373 100644 --- a/server/src/main/java/org/elasticsearch/operator/action/OperatorClusterSettingsAction.java +++ b/server/src/main/java/org/elasticsearch/operator/action/OperatorClusterSettingsAction.java @@ -9,18 +9,15 @@ package org.elasticsearch.operator.action; import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsRequest; -import org.elasticsearch.action.support.master.MasterNodeRequest; +import org.elasticsearch.action.admin.cluster.settings.TransportClusterUpdateSettingsAction; import org.elasticsearch.client.internal.Requests; import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.common.settings.ClusterSettings; import org.elasticsearch.operator.OperatorHandler; -import java.util.Collection; -import java.util.List; import java.util.Map; -import java.util.Optional; import static org.elasticsearch.rest.action.admin.cluster.RestClusterUpdateSettingsAction.PERSISTENT; -import static org.elasticsearch.rest.action.admin.cluster.RestClusterUpdateSettingsAction.TRANSIENT; /** * TODO: Add docs @@ -34,9 +31,8 @@ public String key() { return KEY; } - @Override @SuppressWarnings("unchecked") - public Collection prepare(Object input) { + private ClusterUpdateSettingsRequest prepare(Object input) { if (input instanceof Map source) { final ClusterUpdateSettingsRequest clusterUpdateSettingsRequest = Requests.clusterUpdateSettingsRequest(); @@ -44,16 +40,22 @@ public Collection prepare(Object input) { clusterUpdateSettingsRequest.persistentSettings((Map) source.get(PERSISTENT)); } - return List.of(clusterUpdateSettingsRequest); + return clusterUpdateSettingsRequest; } throw new IllegalStateException("Unsupported " + KEY + " request format"); } @Override - public Optional transformClusterState( - Collection requests, - ClusterState.Builder clusterStateBuilder, - ClusterState previous) { - return Optional.empty(); + public ClusterState transform( + Object input, + ClusterSettings clusterSettings, + ClusterState state) { + + ClusterUpdateSettingsRequest request = prepare(input); + validate(request); + + TransportClusterUpdateSettingsAction.ClusterUpdateSettingsTask updateSettingsTask = + new TransportClusterUpdateSettingsAction.ClusterUpdateSettingsTask(clusterSettings, request); + return updateSettingsTask.execute(state); } } diff --git a/server/src/test/java/org/elasticsearch/operator/OperatorSettingsControllerTests.java b/server/src/test/java/org/elasticsearch/operator/OperatorSettingsControllerTests.java new file mode 100644 index 0000000000000..055270fb7610a --- /dev/null +++ b/server/src/test/java/org/elasticsearch/operator/OperatorSettingsControllerTests.java @@ -0,0 +1,82 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.operator; + +import org.elasticsearch.cluster.ClusterName; +import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.common.settings.ClusterSettings; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.operator.action.OperatorClusterSettingsAction; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xcontent.XContentParser; +import org.elasticsearch.xcontent.XContentParserConfiguration; +import org.elasticsearch.xcontent.XContentType; + +import java.io.IOException; +import java.util.List; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class OperatorSettingsControllerTests extends ESTestCase { + + public void testOperatorController() throws IOException { + ClusterSettings clusterSettings = new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS); + ClusterService clusterService = mock(ClusterService.class); + final ClusterName clusterName = new ClusterName("elasticsearch"); + + ClusterState state = ClusterState.builder(clusterName).build(); + when(clusterService.state()).thenReturn(state); + + OperatorSettingsController controller = new OperatorSettingsController(clusterSettings, clusterService); + controller.initHandlers(List.of(new OperatorClusterSettingsAction())); + + String testJSON = """ + { + "cluster": { + "persistent": { + "indices.recovery.max_bytes_per_sec": "50mb" + }, + "transient": { + "cluster.routing.allocation.enable": "none" + } + }, + "ilm": { + "my_timeseries_lifecycle": { + "policy": { + "phases": { + "warm": { + "min_age": "10s", + "actions": { + "forcemerge": { + "max_num_segments": 10000 + } + } + }, + "delete": { + "min_age": "30s", + "actions": { + "delete": { + + } + } + } + } + } + } + } + } + """; + + try (XContentParser parser = XContentType.JSON.xContent().createParser(XContentParserConfiguration.EMPTY, testJSON)) { + controller.process(parser); + } + } +} diff --git a/test/framework/src/test/java/org/elasticsearch/operator/OperatorSettingsControllerTests.java b/test/framework/src/test/java/org/elasticsearch/operator/OperatorSettingsControllerTests.java deleted file mode 100644 index 432f48ff87b75..0000000000000 --- a/test/framework/src/test/java/org/elasticsearch/operator/OperatorSettingsControllerTests.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * 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 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 or the Server - * Side Public License, v 1. - */ - -package org.elasticsearch.operator; - -import org.elasticsearch.operator.action.OperatorClusterSettingsAction; -import org.elasticsearch.test.ESTestCase; -import org.elasticsearch.xcontent.XContentParser; -import org.elasticsearch.xcontent.XContentParserConfiguration; -import org.elasticsearch.xcontent.XContentType; - -import java.io.IOException; -import java.util.List; - -public class OperatorSettingsControllerTests extends ESTestCase { - - public void testOperatorController() throws IOException { - OperatorSettingsController controller = new OperatorSettingsController(); - controller.initHandlers(List.of(new OperatorClusterSettingsAction())); - - String testJSON = """ - { - "cluster" : { - "persistent" : { - "indices.recovery.max_bytes_per_sec" : "50mb" - }, - "transient" : { - "cluster.routing.allocation.enable": "none" - } - } - } - """; - - try (XContentParser parser = XContentType.JSON.xContent().createParser(XContentParserConfiguration.EMPTY, testJSON)) { - controller.process(parser); - } - } -} diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/operator/OperatorLifecycleAction.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/operator/OperatorLifecycleAction.java index 3028f5d59e019..9fef26f1a693b 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/operator/OperatorLifecycleAction.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/operator/OperatorLifecycleAction.java @@ -8,6 +8,7 @@ package org.elasticsearch.xpack.ilm.action.operator; import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.common.settings.ClusterSettings; import org.elasticsearch.operator.OperatorHandler; import org.elasticsearch.xcontent.XContentParser; import org.elasticsearch.xpack.core.ilm.action.PutLifecycleAction; @@ -62,7 +63,7 @@ private PutLifecycleAction.Request prepare(Map source) throws IOExcep @Override public Optional transformClusterState( Collection requests, - ClusterState.Builder clusterStateBuilder, + ClusterSettings clusterSettings, ClusterState previous) { return Optional.empty(); } From 764505e429a0f4051939f52106c8750e4603333a Mon Sep 17 00:00:00 2001 From: Nikola Grcevski Date: Mon, 25 Apr 2022 16:55:17 -0400 Subject: [PATCH 04/48] More refactoring --- .../elasticsearch/action/ActionModule.java | 8 +- .../TransportClusterUpdateSettingsAction.java | 203 +++++++++--------- .../operator/OperatorHandler.java | 18 +- .../operator/OperatorSettingsController.java | 17 +- ... OperatorClusterUpdateSettingsAction.java} | 26 +-- .../elasticsearch/plugins/ActionPlugin.java | 2 +- .../action/ActionModuleTests.java | 4 + .../OperatorSettingsControllerTests.java | 6 +- .../xpack/ilm/IndexLifecycle.java | 8 +- .../action/TransportPutLifecycleAction.java | 139 +++++++----- .../operator/OperatorLifecycleAction.java | 70 ------ .../operator/OperatorPutLifecycleAction.java | 81 +++++++ .../xpack/security/SecurityTests.java | 1 + 13 files changed, 316 insertions(+), 267 deletions(-) rename server/src/main/java/org/elasticsearch/operator/action/{OperatorClusterSettingsAction.java => OperatorClusterUpdateSettingsAction.java} (68%) delete mode 100644 x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/operator/OperatorLifecycleAction.java create mode 100644 x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/operator/OperatorPutLifecycleAction.java diff --git a/server/src/main/java/org/elasticsearch/action/ActionModule.java b/server/src/main/java/org/elasticsearch/action/ActionModule.java index ac2b95e35017e..014b687506576 100644 --- a/server/src/main/java/org/elasticsearch/action/ActionModule.java +++ b/server/src/main/java/org/elasticsearch/action/ActionModule.java @@ -265,7 +265,7 @@ import org.elasticsearch.indices.store.TransportNodesListShardStoreMetadata; import org.elasticsearch.operator.OperatorHandler; import org.elasticsearch.operator.OperatorSettingsController; -import org.elasticsearch.operator.action.OperatorClusterSettingsAction; +import org.elasticsearch.operator.action.OperatorClusterUpdateSettingsAction; import org.elasticsearch.persistent.CompletionPersistentTaskAction; import org.elasticsearch.persistent.RemovePersistentTaskAction; import org.elasticsearch.persistent.StartPersistentTaskAction; @@ -510,7 +510,7 @@ public ActionModule( ); restController = new RestController(headers, restWrapper, nodeClient, circuitBreakerService, usageService); - operatorController = new OperatorSettingsController(clusterSettings, clusterService); + operatorController = new OperatorSettingsController(clusterService); } public Map> getActions() { @@ -884,9 +884,9 @@ public void initRestHandlers(Supplier nodesInCluster) { } public void initOperatorHandlers() { - List handlers = new ArrayList<>(); + List> handlers = new ArrayList<>(); - handlers.add(new OperatorClusterSettingsAction()); + handlers.add(new OperatorClusterUpdateSettingsAction(clusterSettings)); for (ActionPlugin plugin : actionPlugins) { handlers.addAll(plugin.getOperatorHandlers()); } diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/settings/TransportClusterUpdateSettingsAction.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/settings/TransportClusterUpdateSettingsAction.java index a5753732d51c5..887784f9b0c8c 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/cluster/settings/TransportClusterUpdateSettingsAction.java +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/settings/TransportClusterUpdateSettingsAction.java @@ -20,7 +20,6 @@ import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.ClusterStateTaskExecutor; import org.elasticsearch.cluster.ClusterStateUpdateTask; -import org.elasticsearch.cluster.ack.AckedRequest; import org.elasticsearch.cluster.block.ClusterBlockException; import org.elasticsearch.cluster.block.ClusterBlockLevel; import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; @@ -146,118 +145,121 @@ protected void masterOperation( final ClusterState state, final ActionListener listener ) { - clusterService.submitStateUpdateTask(UPDATE_TASK_SOURCE, new ClusterUpdateSettingsTask( - clusterSettings, - Priority.IMMEDIATE, - request, - listener - ) { - @Override - protected ClusterUpdateSettingsResponse newResponse(boolean acknowledged) { - return new ClusterUpdateSettingsResponse(acknowledged, updater.getTransientUpdates(), updater.getPersistentUpdate()); - } - - @Override - public void onAllNodesAcked() { - if (changed) { - reroute(true); - } else { - super.onAllNodesAcked(); + clusterService.submitStateUpdateTask( + UPDATE_TASK_SOURCE, + new ClusterUpdateSettingsTask(clusterSettings, Priority.IMMEDIATE, request, listener) { + @Override + protected ClusterUpdateSettingsResponse newResponse(boolean acknowledged) { + return new ClusterUpdateSettingsResponse(acknowledged, updater.getTransientUpdates(), updater.getPersistentUpdate()); } - } - @Override - public void onAckFailure(Exception e) { - if (changed) { - reroute(true); - } else { - super.onAckFailure(e); + @Override + public void onAllNodesAcked() { + if (changed) { + reroute(true); + } else { + super.onAllNodesAcked(); + } } - } - @Override - public void onAckTimeout() { - if (changed) { - reroute(false); - } else { - super.onAckTimeout(); + @Override + public void onAckFailure(Exception e) { + if (changed) { + reroute(true); + } else { + super.onAckFailure(e); + } } - } - private void reroute(final boolean updateSettingsAcked) { - // We're about to send a second update task, so we need to check if we're still the elected master - // For example the minimum_master_node could have been breached and we're no longer elected master, - // so we should *not* execute the reroute. - if (clusterService.state().nodes().isLocalNodeElectedMaster() == false) { - logger.debug("Skipping reroute after cluster update settings, because node is no longer master"); - listener.onResponse( - new ClusterUpdateSettingsResponse(updateSettingsAcked, updater.getTransientUpdates(), updater.getPersistentUpdate()) - ); - return; + @Override + public void onAckTimeout() { + if (changed) { + reroute(false); + } else { + super.onAckTimeout(); + } } - // The reason the reroute needs to be send as separate update task, is that all the *cluster* settings are encapsulate - // in the components (e.g. FilterAllocationDecider), so the changes made by the first call aren't visible - // to the components until the ClusterStateListener instances have been invoked, but are visible after - // the first update task has been completed. - clusterService.submitStateUpdateTask( - REROUTE_TASK_SOURCE, - new AckedClusterStateUpdateTask(Priority.URGENT, request, listener) { - - @Override - public boolean mustAck(DiscoveryNode discoveryNode) { - // we wait for the reroute ack only if the update settings was acknowledged - return updateSettingsAcked; - } - - @Override - // we return when the cluster reroute is acked or it times out but the acknowledged flag depends on whether the - // update settings was acknowledged - protected ClusterUpdateSettingsResponse newResponse(boolean acknowledged) { - return new ClusterUpdateSettingsResponse( - updateSettingsAcked && acknowledged, + private void reroute(final boolean updateSettingsAcked) { + // We're about to send a second update task, so we need to check if we're still the elected master + // For example the minimum_master_node could have been breached and we're no longer elected master, + // so we should *not* execute the reroute. + if (clusterService.state().nodes().isLocalNodeElectedMaster() == false) { + logger.debug("Skipping reroute after cluster update settings, because node is no longer master"); + listener.onResponse( + new ClusterUpdateSettingsResponse( + updateSettingsAcked, updater.getTransientUpdates(), updater.getPersistentUpdate() - ); - } + ) + ); + return; + } + + // The reason the reroute needs to be send as separate update task, is that all the *cluster* settings are encapsulate + // in the components (e.g. FilterAllocationDecider), so the changes made by the first call aren't visible + // to the components until the ClusterStateListener instances have been invoked, but are visible after + // the first update task has been completed. + clusterService.submitStateUpdateTask( + REROUTE_TASK_SOURCE, + new AckedClusterStateUpdateTask(Priority.URGENT, request, listener) { + + @Override + public boolean mustAck(DiscoveryNode discoveryNode) { + // we wait for the reroute ack only if the update settings was acknowledged + return updateSettingsAcked; + } - @Override - public void onNoLongerMaster() { - logger.debug( - "failed to preform reroute after cluster settings were updated - current node is no longer a master" - ); - listener.onResponse( - new ClusterUpdateSettingsResponse( - updateSettingsAcked, + @Override + // we return when the cluster reroute is acked or it times out but the acknowledged flag depends on whether the + // update settings was acknowledged + protected ClusterUpdateSettingsResponse newResponse(boolean acknowledged) { + return new ClusterUpdateSettingsResponse( + updateSettingsAcked && acknowledged, updater.getTransientUpdates(), updater.getPersistentUpdate() - ) - ); - } + ); + } - @Override - public void onFailure(Exception e) { - // if the reroute fails we only log - logger.debug(() -> new ParameterizedMessage("failed to perform [{}]", REROUTE_TASK_SOURCE), e); - listener.onFailure(new ElasticsearchException("reroute after update settings failed", e)); - } + @Override + public void onNoLongerMaster() { + logger.debug( + "failed to preform reroute after cluster settings were updated - current node is no longer a master" + ); + listener.onResponse( + new ClusterUpdateSettingsResponse( + updateSettingsAcked, + updater.getTransientUpdates(), + updater.getPersistentUpdate() + ) + ); + } - @Override - public ClusterState execute(final ClusterState currentState) { - // now, reroute in case things that require it changed (e.g. number of replicas) - return allocationService.reroute(currentState, "reroute after cluster update settings"); - } - }, - newExecutor() - ); - } + @Override + public void onFailure(Exception e) { + // if the reroute fails we only log + logger.debug(() -> new ParameterizedMessage("failed to perform [{}]", REROUTE_TASK_SOURCE), e); + listener.onFailure(new ElasticsearchException("reroute after update settings failed", e)); + } - @Override - public void onFailure(Exception e) { - logger.debug(() -> new ParameterizedMessage("failed to perform [{}]", UPDATE_TASK_SOURCE), e); - super.onFailure(e); - } - }, newExecutor()); + @Override + public ClusterState execute(final ClusterState currentState) { + // now, reroute in case things that require it changed (e.g. number of replicas) + return allocationService.reroute(currentState, "reroute after cluster update settings"); + } + }, + newExecutor() + ); + } + + @Override + public void onFailure(Exception e) { + logger.debug(() -> new ParameterizedMessage("failed to perform [{}]", UPDATE_TASK_SOURCE), e); + super.onFailure(e); + } + }, + newExecutor() + ); } public static class ClusterUpdateSettingsTask extends AckedClusterStateUpdateTask { @@ -270,16 +272,15 @@ public ClusterUpdateSettingsTask( final ClusterSettings clusterSettings, Priority priority, ClusterUpdateSettingsRequest request, - ActionListener listener) { + ActionListener listener + ) { super(priority, request, listener); this.clusterSettings = clusterSettings; this.updater = new SettingsUpdater(clusterSettings); this.request = request; } - public ClusterUpdateSettingsTask( - final ClusterSettings clusterSettings, - ClusterUpdateSettingsRequest request) { + public ClusterUpdateSettingsTask(final ClusterSettings clusterSettings, ClusterUpdateSettingsRequest request) { this(clusterSettings, Priority.IMMEDIATE, request, null); } diff --git a/server/src/main/java/org/elasticsearch/operator/OperatorHandler.java b/server/src/main/java/org/elasticsearch/operator/OperatorHandler.java index be37451a373d7..0e8fe9d4951f7 100644 --- a/server/src/main/java/org/elasticsearch/operator/OperatorHandler.java +++ b/server/src/main/java/org/elasticsearch/operator/OperatorHandler.java @@ -13,7 +13,6 @@ import org.elasticsearch.action.support.master.MasterNodeRequest; import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.common.Strings; -import org.elasticsearch.common.settings.ClusterSettings; import org.elasticsearch.xcontent.XContentBuilder; import org.elasticsearch.xcontent.XContentFactory; import org.elasticsearch.xcontent.XContentParser; @@ -24,20 +23,18 @@ import java.util.Collection; import java.util.Collections; import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; /** * TODO: Add docs */ -public interface OperatorHandler { +public interface OperatorHandler> { String CONTENT = "content"; String key(); - ClusterState transform( - Object source, - ClusterSettings clusterSettings, - ClusterState state - ); + ClusterState transform(Object source, ClusterState state) throws Exception; default Collection dependencies() { return Collections.emptyList(); @@ -50,6 +47,13 @@ default void validate(T request) { } } + default Map asMap(Object input) { + if (input instanceof Map source) { + return source.entrySet().stream().collect(Collectors.toMap(Object::toString, Function.identity())); + } + throw new IllegalStateException("Unsupported " + key() + " request format"); + } + default XContentParser mapToXContentParser(Map source) { try (XContentBuilder builder = XContentFactory.contentBuilder(XContentType.JSON)) { builder.map(source); diff --git a/server/src/main/java/org/elasticsearch/operator/OperatorSettingsController.java b/server/src/main/java/org/elasticsearch/operator/OperatorSettingsController.java index c3bf644c35cd3..b18f362b2e7c7 100644 --- a/server/src/main/java/org/elasticsearch/operator/OperatorSettingsController.java +++ b/server/src/main/java/org/elasticsearch/operator/OperatorSettingsController.java @@ -10,7 +10,6 @@ import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.service.ClusterService; -import org.elasticsearch.common.settings.ClusterSettings; import org.elasticsearch.xcontent.XContentParser; import java.io.IOException; @@ -26,16 +25,14 @@ * TODO: Write docs */ public class OperatorSettingsController { - Map handlers = null; - final ClusterSettings clusterSettings; + Map> handlers = null; final ClusterService clusterService; - public OperatorSettingsController(ClusterSettings clusterSettings, ClusterService clusterService) { - this.clusterSettings = clusterSettings; + public OperatorSettingsController(ClusterService clusterService) { this.clusterService = clusterService; } - public void initHandlers(List handlerList) { + public void initHandlers(List> handlerList) { handlers = handlerList.stream().collect(Collectors.toMap(OperatorHandler::key, Function.identity())); } @@ -47,8 +44,12 @@ public ClusterState process(XContentParser parser) throws IOException { AtomicReference state = new AtomicReference<>(clusterService.state()); orderedHandler.forEach(k -> { - OperatorHandler handler = handlers.get(k); - state.set(handler.transform(source.get(k), clusterSettings, state.get())); + OperatorHandler handler = handlers.get(k); + try { + state.set(handler.transform(source.get(k), state.get())); + } catch (Exception e) { + throw new IllegalStateException("Error processing state change request for: " + handler.key(), e); + } }); return null; diff --git a/server/src/main/java/org/elasticsearch/operator/action/OperatorClusterSettingsAction.java b/server/src/main/java/org/elasticsearch/operator/action/OperatorClusterUpdateSettingsAction.java similarity index 68% rename from server/src/main/java/org/elasticsearch/operator/action/OperatorClusterSettingsAction.java rename to server/src/main/java/org/elasticsearch/operator/action/OperatorClusterUpdateSettingsAction.java index 30a9f2dc6e373..d5be5ddcaf98c 100644 --- a/server/src/main/java/org/elasticsearch/operator/action/OperatorClusterSettingsAction.java +++ b/server/src/main/java/org/elasticsearch/operator/action/OperatorClusterUpdateSettingsAction.java @@ -22,10 +22,16 @@ /** * TODO: Add docs */ -public class OperatorClusterSettingsAction implements OperatorHandler { +public class OperatorClusterUpdateSettingsAction implements OperatorHandler { public static final String KEY = "cluster"; + private final ClusterSettings clusterSettings; + + public OperatorClusterUpdateSettingsAction(ClusterSettings clusterSettings) { + this.clusterSettings = clusterSettings; + } + @Override public String key() { return KEY; @@ -33,23 +39,19 @@ public String key() { @SuppressWarnings("unchecked") private ClusterUpdateSettingsRequest prepare(Object input) { - if (input instanceof Map source) { - final ClusterUpdateSettingsRequest clusterUpdateSettingsRequest = Requests.clusterUpdateSettingsRequest(); + final ClusterUpdateSettingsRequest clusterUpdateSettingsRequest = Requests.clusterUpdateSettingsRequest(); - if (source.containsKey(PERSISTENT)) { - clusterUpdateSettingsRequest.persistentSettings((Map) source.get(PERSISTENT)); - } + Map source = asMap(input); - return clusterUpdateSettingsRequest; + if (source.containsKey(PERSISTENT)) { + clusterUpdateSettingsRequest.persistentSettings((Map) source.get(PERSISTENT)); } - throw new IllegalStateException("Unsupported " + KEY + " request format"); + + return clusterUpdateSettingsRequest; } @Override - public ClusterState transform( - Object input, - ClusterSettings clusterSettings, - ClusterState state) { + public ClusterState transform(Object input, ClusterState state) { ClusterUpdateSettingsRequest request = prepare(input); validate(request); diff --git a/server/src/main/java/org/elasticsearch/plugins/ActionPlugin.java b/server/src/main/java/org/elasticsearch/plugins/ActionPlugin.java index fe913bf2071f7..a43b5560fa7a1 100644 --- a/server/src/main/java/org/elasticsearch/plugins/ActionPlugin.java +++ b/server/src/main/java/org/elasticsearch/plugins/ActionPlugin.java @@ -85,7 +85,7 @@ default List getRestHandlers( return Collections.emptyList(); } - default List getOperatorHandlers() { + default List> getOperatorHandlers() { return Collections.emptyList(); } diff --git a/server/src/test/java/org/elasticsearch/action/ActionModuleTests.java b/server/src/test/java/org/elasticsearch/action/ActionModuleTests.java index 0dd385a069208..7142adc106b74 100644 --- a/server/src/test/java/org/elasticsearch/action/ActionModuleTests.java +++ b/server/src/test/java/org/elasticsearch/action/ActionModuleTests.java @@ -115,6 +115,7 @@ public void testSetupRestHandlerContainsKnownBuiltin() { null, null, usageService, + null, null ); actionModule.initRestHandlers(null); @@ -171,6 +172,7 @@ public String getName() { null, null, usageService, + null, null ); Exception e = expectThrows(IllegalArgumentException.class, () -> actionModule.initRestHandlers(null)); @@ -220,6 +222,7 @@ public List getRestHandlers( null, null, usageService, + null, null ); actionModule.initRestHandlers(null); @@ -264,6 +267,7 @@ public void test3rdPartyHandlerIsNotInstalled() { null, null, usageService, + null, null ) ); diff --git a/server/src/test/java/org/elasticsearch/operator/OperatorSettingsControllerTests.java b/server/src/test/java/org/elasticsearch/operator/OperatorSettingsControllerTests.java index 055270fb7610a..e550ba4e7a4b3 100644 --- a/server/src/test/java/org/elasticsearch/operator/OperatorSettingsControllerTests.java +++ b/server/src/test/java/org/elasticsearch/operator/OperatorSettingsControllerTests.java @@ -13,7 +13,7 @@ import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.settings.ClusterSettings; import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.operator.action.OperatorClusterSettingsAction; +import org.elasticsearch.operator.action.OperatorClusterUpdateSettingsAction; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.xcontent.XContentParser; import org.elasticsearch.xcontent.XContentParserConfiguration; @@ -35,8 +35,8 @@ public void testOperatorController() throws IOException { ClusterState state = ClusterState.builder(clusterName).build(); when(clusterService.state()).thenReturn(state); - OperatorSettingsController controller = new OperatorSettingsController(clusterSettings, clusterService); - controller.initHandlers(List.of(new OperatorClusterSettingsAction())); + OperatorSettingsController controller = new OperatorSettingsController(clusterService); + controller.initHandlers(List.of(new OperatorClusterUpdateSettingsAction(clusterSettings))); String testJSON = """ { diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycle.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycle.java index 16d69af053358..f6a2f47fd6ac7 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycle.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycle.java @@ -106,7 +106,7 @@ import org.elasticsearch.xpack.ilm.action.TransportRetryAction; import org.elasticsearch.xpack.ilm.action.TransportStartILMAction; import org.elasticsearch.xpack.ilm.action.TransportStopILMAction; -import org.elasticsearch.xpack.ilm.action.operator.OperatorLifecycleAction; +import org.elasticsearch.xpack.ilm.action.operator.OperatorPutLifecycleAction; import org.elasticsearch.xpack.ilm.history.ILMHistoryStore; import org.elasticsearch.xpack.ilm.history.ILMHistoryTemplateRegistry; import org.elasticsearch.xpack.slm.SLMInfoTransportAction; @@ -160,6 +160,7 @@ public class IndexLifecycle extends Plugin implements ActionPlugin, HealthPlugin private final SetOnce snapshotHistoryStore = new SetOnce<>(); private final SetOnce ilmHealthIndicatorService = new SetOnce<>(); private final SetOnce slmHealthIndicatorService = new SetOnce<>(); + private final SetOnce ilmOperatorAction = new SetOnce<>(); private final Settings settings; public IndexLifecycle(Settings settings) { @@ -273,6 +274,7 @@ public Collection createComponents( components.addAll(Arrays.asList(snapshotLifecycleService.get(), snapshotHistoryStore.get(), snapshotRetentionService.get())); ilmHealthIndicatorService.set(new IlmHealthIndicatorService(clusterService)); slmHealthIndicatorService.set(new SlmHealthIndicatorService(clusterService)); + ilmOperatorAction.set(new OperatorPutLifecycleAction(xContentRegistry, client, XPackPlugin.getSharedLicenseState())); return components; } @@ -381,8 +383,8 @@ public List getRestHandlers( } @Override - public List getOperatorHandlers() { - return List.of(new OperatorLifecycleAction()); + public List> getOperatorHandlers() { + return List.of(ilmOperatorAction.get()); } @Override diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportPutLifecycleAction.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportPutLifecycleAction.java index 35477bf432e24..360d0f4b513e4 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportPutLifecycleAction.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportPutLifecycleAction.java @@ -45,6 +45,7 @@ import org.elasticsearch.xpack.core.slm.SnapshotLifecycleMetadata; import java.time.Instant; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.SortedMap; @@ -113,69 +114,91 @@ protected void masterOperation(Task task, Request request, ClusterState state, A clusterService.submitStateUpdateTask( "put-lifecycle-" + request.getPolicy().getName(), - new AckedClusterStateUpdateTask(request, listener) { - @Override - public ClusterState execute(ClusterState currentState) throws Exception { - final IndexLifecycleMetadata currentMetadata = currentState.metadata() - .custom(IndexLifecycleMetadata.TYPE, IndexLifecycleMetadata.EMPTY); - final LifecyclePolicyMetadata existingPolicyMetadata = currentMetadata.getPolicyMetadatas() - .get(request.getPolicy().getName()); + new UpdateLifecycleTask(request, listener, licenseState, filteredHeaders, xContentRegistry, client), + newExecutor() + ); + } + + public static class UpdateLifecycleTask extends AckedClusterStateUpdateTask { + private final Request request; + private final XPackLicenseState licenseState; + private final Map filteredHeaders; + private final NamedXContentRegistry xContentRegistry; + private final Client client; + + public UpdateLifecycleTask( + Request request, + ActionListener listener, + XPackLicenseState licenseState, + Map filteredHeaders, + NamedXContentRegistry xContentRegistry, + Client client + ) { + super(request, listener); + this.request = request; + this.licenseState = licenseState; + this.filteredHeaders = filteredHeaders; + this.xContentRegistry = xContentRegistry; + this.client = client; + } + + public UpdateLifecycleTask(Request request, XPackLicenseState licenseState, NamedXContentRegistry xContentRegistry, Client client) { + this(request, null, licenseState, new HashMap<>(), xContentRegistry, client); + } - // Double-check for no-op in the state update task, in case it was changed/reset in the meantime - if (isNoopUpdate(existingPolicyMetadata, request.getPolicy(), filteredHeaders)) { - return currentState; - } + @Override + public ClusterState execute(ClusterState currentState) throws Exception { + final IndexLifecycleMetadata currentMetadata = currentState.metadata() + .custom(IndexLifecycleMetadata.TYPE, IndexLifecycleMetadata.EMPTY); + final LifecyclePolicyMetadata existingPolicyMetadata = currentMetadata.getPolicyMetadatas().get(request.getPolicy().getName()); - validatePrerequisites(request.getPolicy(), currentState); + // Double-check for no-op in the state update task, in case it was changed/reset in the meantime + if (isNoopUpdate(existingPolicyMetadata, request.getPolicy(), filteredHeaders)) { + return currentState; + } + + validatePrerequisites(request.getPolicy(), currentState, licenseState); - ClusterState.Builder stateBuilder = ClusterState.builder(currentState); - long nextVersion = (existingPolicyMetadata == null) ? 1L : existingPolicyMetadata.getVersion() + 1L; - SortedMap newPolicies = new TreeMap<>(currentMetadata.getPolicyMetadatas()); - LifecyclePolicyMetadata lifecyclePolicyMetadata = new LifecyclePolicyMetadata( - request.getPolicy(), - filteredHeaders, - nextVersion, - Instant.now().toEpochMilli() + ClusterState.Builder stateBuilder = ClusterState.builder(currentState); + long nextVersion = (existingPolicyMetadata == null) ? 1L : existingPolicyMetadata.getVersion() + 1L; + SortedMap newPolicies = new TreeMap<>(currentMetadata.getPolicyMetadatas()); + LifecyclePolicyMetadata lifecyclePolicyMetadata = new LifecyclePolicyMetadata( + request.getPolicy(), + filteredHeaders, + nextVersion, + Instant.now().toEpochMilli() + ); + LifecyclePolicyMetadata oldPolicy = newPolicies.put(lifecyclePolicyMetadata.getName(), lifecyclePolicyMetadata); + if (oldPolicy == null) { + logger.info("adding index lifecycle policy [{}]", request.getPolicy().getName()); + } else { + logger.info("updating index lifecycle policy [{}]", request.getPolicy().getName()); + } + IndexLifecycleMetadata newMetadata = new IndexLifecycleMetadata(newPolicies, currentMetadata.getOperationMode()); + stateBuilder.metadata(Metadata.builder(currentState.getMetadata()).putCustom(IndexLifecycleMetadata.TYPE, newMetadata).build()); + ClusterState nonRefreshedState = stateBuilder.build(); + if (oldPolicy == null) { + return nonRefreshedState; + } else { + try { + return updateIndicesForPolicy( + nonRefreshedState, + xContentRegistry, + client, + oldPolicy.getPolicy(), + lifecyclePolicyMetadata, + licenseState ); - LifecyclePolicyMetadata oldPolicy = newPolicies.put(lifecyclePolicyMetadata.getName(), lifecyclePolicyMetadata); - if (oldPolicy == null) { - logger.info("adding index lifecycle policy [{}]", request.getPolicy().getName()); - } else { - logger.info("updating index lifecycle policy [{}]", request.getPolicy().getName()); - } - IndexLifecycleMetadata newMetadata = new IndexLifecycleMetadata(newPolicies, currentMetadata.getOperationMode()); - stateBuilder.metadata( - Metadata.builder(currentState.getMetadata()).putCustom(IndexLifecycleMetadata.TYPE, newMetadata).build() + } catch (Exception e) { + logger.warn( + new ParameterizedMessage("unable to refresh indices phase JSON for updated policy [{}]", oldPolicy.getName()), + e ); - ClusterState nonRefreshedState = stateBuilder.build(); - if (oldPolicy == null) { - return nonRefreshedState; - } else { - try { - return updateIndicesForPolicy( - nonRefreshedState, - xContentRegistry, - client, - oldPolicy.getPolicy(), - lifecyclePolicyMetadata, - licenseState - ); - } catch (Exception e) { - logger.warn( - new ParameterizedMessage( - "unable to refresh indices phase JSON for updated policy [{}]", - oldPolicy.getName() - ), - e - ); - // Revert to the non-refreshed state - return nonRefreshedState; - } - } + // Revert to the non-refreshed state + return nonRefreshedState; } - }, - newExecutor() - ); + } + } } @SuppressForbidden(reason = "legacy usage of unbatched task") // TODO add support for batching here @@ -205,7 +228,7 @@ static boolean isNoopUpdate( * @param policy The lifecycle policy * @param state The cluster state */ - private void validatePrerequisites(LifecyclePolicy policy, ClusterState state) { + private static void validatePrerequisites(LifecyclePolicy policy, ClusterState state, XPackLicenseState licenseState) { List phasesWithSearchableSnapshotActions = policy.getPhases() .values() .stream() diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/operator/OperatorLifecycleAction.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/operator/OperatorLifecycleAction.java deleted file mode 100644 index 9fef26f1a693b..0000000000000 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/operator/OperatorLifecycleAction.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * 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.ilm.action.operator; - -import org.elasticsearch.cluster.ClusterState; -import org.elasticsearch.common.settings.ClusterSettings; -import org.elasticsearch.operator.OperatorHandler; -import org.elasticsearch.xcontent.XContentParser; -import org.elasticsearch.xpack.core.ilm.action.PutLifecycleAction; -import org.elasticsearch.xpack.ilm.action.RestPutLifecycleAction; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.Optional; - -/** - * TODO: Add docs - */ -public class OperatorLifecycleAction implements OperatorHandler { - - public static final String KEY = "ilm"; - - @Override - public String key() { - return KEY; - } - - @Override - @SuppressWarnings("unchecked") - public Collection prepare(Object source) throws IOException { - List result = new ArrayList<>(); - - if (source.getClass().isArray()) { - Map[] sources = (Map[]) source; - for (Map s : sources) { - result.add(prepare(s)); - } - } else { - result.add(prepare((Map) source)); - } - - return result; - } - - @SuppressWarnings("unchecked") - private PutLifecycleAction.Request prepare(Map source) throws IOException { - String lifecycleName = (String) source.get(RestPutLifecycleAction.NAME); - Map content = (Map) source.get(OperatorHandler.CONTENT); - - try (XContentParser parser = mapToXContentParser(content)) { - return PutLifecycleAction.Request.parseRequest(lifecycleName, parser); - } - } - - @Override - public Optional transformClusterState( - Collection requests, - ClusterSettings clusterSettings, - ClusterState previous) { - return Optional.empty(); - } -} diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/operator/OperatorPutLifecycleAction.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/operator/OperatorPutLifecycleAction.java new file mode 100644 index 0000000000000..4c133255bcd3d --- /dev/null +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/operator/OperatorPutLifecycleAction.java @@ -0,0 +1,81 @@ +/* + * 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.ilm.action.operator; + +import org.elasticsearch.client.internal.Client; +import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.license.XPackLicenseState; +import org.elasticsearch.operator.OperatorHandler; +import org.elasticsearch.xcontent.NamedXContentRegistry; +import org.elasticsearch.xcontent.XContentParser; +import org.elasticsearch.xpack.core.ilm.action.PutLifecycleAction; +import org.elasticsearch.xpack.ilm.action.TransportPutLifecycleAction; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + * TODO: Add docs + */ +public class OperatorPutLifecycleAction implements OperatorHandler { + + private final NamedXContentRegistry xContentRegistry; + private final Client client; + private final XPackLicenseState licenseState; + + public static final String KEY = "ilm"; + + public OperatorPutLifecycleAction(NamedXContentRegistry xContentRegistry, Client client, XPackLicenseState licenseState) { + this.xContentRegistry = xContentRegistry; + this.client = client; + this.licenseState = licenseState; + } + + @Override + public String key() { + return KEY; + } + + @SuppressWarnings("unchecked") + public Collection prepare(Object input) throws IOException { + List result = new ArrayList<>(); + + Map source = asMap(input); + + for (String name : source.keySet()) { + Map content = (Map) source.get(name); + try (XContentParser parser = mapToXContentParser(content)) { + PutLifecycleAction.Request request = PutLifecycleAction.Request.parseRequest(name, parser); + validate(request); + result.add(request); + } + } + + return result; + } + + @Override + public ClusterState transform(Object source, ClusterState state) throws Exception { + var requests = prepare(source); + + for (var request : requests) { + TransportPutLifecycleAction.UpdateLifecycleTask task = new TransportPutLifecycleAction.UpdateLifecycleTask( + request, + licenseState, + xContentRegistry, + client + ); + + state = task.execute(state); + } + return state; + } +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/SecurityTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/SecurityTests.java index b633a7efe7035..99d33fbc72f4a 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/SecurityTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/SecurityTests.java @@ -704,6 +704,7 @@ public void testSecurityRestHandlerWrapperCanBeInstalled() throws IllegalAccessE null, null, usageService, + null, null ); actionModule.initRestHandlers(null); From 31b5ae37e52880c7267ad00985fd9a203f507567 Mon Sep 17 00:00:00 2001 From: Nikola Grcevski Date: Tue, 26 Apr 2022 17:03:56 -0400 Subject: [PATCH 05/48] Make ILM do processing --- .../elasticsearch/action/ActionModule.java | 6 +- ...va => OperatorClusterStateController.java} | 27 ++-- .../operator/OperatorHandler.java | 5 +- ... OperatorClusterStateControllerTests.java} | 30 +--- .../operator/OperatorILMControllerTests.java | 153 ++++++++++++++++++ 5 files changed, 177 insertions(+), 44 deletions(-) rename server/src/main/java/org/elasticsearch/operator/{OperatorSettingsController.java => OperatorClusterStateController.java} (78%) rename server/src/test/java/org/elasticsearch/operator/{OperatorSettingsControllerTests.java => OperatorClusterStateControllerTests.java} (65%) create mode 100644 x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/action/operator/OperatorILMControllerTests.java diff --git a/server/src/main/java/org/elasticsearch/action/ActionModule.java b/server/src/main/java/org/elasticsearch/action/ActionModule.java index 014b687506576..267f1a4c00515 100644 --- a/server/src/main/java/org/elasticsearch/action/ActionModule.java +++ b/server/src/main/java/org/elasticsearch/action/ActionModule.java @@ -263,8 +263,8 @@ import org.elasticsearch.indices.SystemIndices; import org.elasticsearch.indices.breaker.CircuitBreakerService; import org.elasticsearch.indices.store.TransportNodesListShardStoreMetadata; +import org.elasticsearch.operator.OperatorClusterStateController; import org.elasticsearch.operator.OperatorHandler; -import org.elasticsearch.operator.OperatorSettingsController; import org.elasticsearch.operator.action.OperatorClusterUpdateSettingsAction; import org.elasticsearch.persistent.CompletionPersistentTaskAction; import org.elasticsearch.persistent.RemovePersistentTaskAction; @@ -445,7 +445,7 @@ public class ActionModule extends AbstractModule { private final RequestValidators mappingRequestValidators; private final RequestValidators indicesAliasesRequestRequestValidators; private final ThreadPool threadPool; - private final OperatorSettingsController operatorController; + private final OperatorClusterStateController operatorController; private final ClusterService clusterService; public ActionModule( @@ -510,7 +510,7 @@ public ActionModule( ); restController = new RestController(headers, restWrapper, nodeClient, circuitBreakerService, usageService); - operatorController = new OperatorSettingsController(clusterService); + operatorController = new OperatorClusterStateController(clusterService); } public Map> getActions() { diff --git a/server/src/main/java/org/elasticsearch/operator/OperatorSettingsController.java b/server/src/main/java/org/elasticsearch/operator/OperatorClusterStateController.java similarity index 78% rename from server/src/main/java/org/elasticsearch/operator/OperatorSettingsController.java rename to server/src/main/java/org/elasticsearch/operator/OperatorClusterStateController.java index b18f362b2e7c7..34c249a6124a3 100644 --- a/server/src/main/java/org/elasticsearch/operator/OperatorSettingsController.java +++ b/server/src/main/java/org/elasticsearch/operator/OperatorClusterStateController.java @@ -17,18 +17,17 @@ import java.util.List; import java.util.Map; import java.util.Set; -import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; import java.util.stream.Collectors; /** * TODO: Write docs */ -public class OperatorSettingsController { +public class OperatorClusterStateController { Map> handlers = null; final ClusterService clusterService; - public OperatorSettingsController(ClusterService clusterService) { + public OperatorClusterStateController(ClusterService clusterService) { this.clusterService = clusterService; } @@ -36,23 +35,29 @@ public void initHandlers(List> handlerList) { handlers = handlerList.stream().collect(Collectors.toMap(OperatorHandler::key, Function.identity())); } - public ClusterState process(XContentParser parser) throws IOException { + public ClusterState process(String namespace, XContentParser parser) throws IOException { Map source = parser.map(); - LinkedHashSet orderedHandler = orderedStateHandlers(source.keySet()); + LinkedHashSet orderedHandlers = orderedStateHandlers(source.keySet()); - AtomicReference state = new AtomicReference<>(clusterService.state()); + ClusterState state = clusterService.state(); - orderedHandler.forEach(k -> { - OperatorHandler handler = handlers.get(k); + // TODO: extract the namespace keys from the state, if any and pass them to each transform + + for (var handlerKey : orderedHandlers) { + OperatorHandler handler = handlers.get(handlerKey); try { - state.set(handler.transform(source.get(k), state.get())); + state = handler.transform(source.get(handlerKey), state); } catch (Exception e) { throw new IllegalStateException("Error processing state change request for: " + handler.key(), e); } - }); + } + + // TODO: extract the keys written for this namespace, and store them in the cluster state + // TODO: call a clusterService state update task + // TODO: call reroute service - return null; + return state; } LinkedHashSet orderedStateHandlers(Set keys) { diff --git a/server/src/main/java/org/elasticsearch/operator/OperatorHandler.java b/server/src/main/java/org/elasticsearch/operator/OperatorHandler.java index 0e8fe9d4951f7..7a41b026b0337 100644 --- a/server/src/main/java/org/elasticsearch/operator/OperatorHandler.java +++ b/server/src/main/java/org/elasticsearch/operator/OperatorHandler.java @@ -23,8 +23,6 @@ import java.util.Collection; import java.util.Collections; import java.util.Map; -import java.util.function.Function; -import java.util.stream.Collectors; /** * TODO: Add docs @@ -47,9 +45,10 @@ default void validate(T request) { } } + @SuppressWarnings("unchecked") default Map asMap(Object input) { if (input instanceof Map source) { - return source.entrySet().stream().collect(Collectors.toMap(Object::toString, Function.identity())); + return (Map) source; } throw new IllegalStateException("Unsupported " + key() + " request format"); } diff --git a/server/src/test/java/org/elasticsearch/operator/OperatorSettingsControllerTests.java b/server/src/test/java/org/elasticsearch/operator/OperatorClusterStateControllerTests.java similarity index 65% rename from server/src/test/java/org/elasticsearch/operator/OperatorSettingsControllerTests.java rename to server/src/test/java/org/elasticsearch/operator/OperatorClusterStateControllerTests.java index e550ba4e7a4b3..c8c39f515deb0 100644 --- a/server/src/test/java/org/elasticsearch/operator/OperatorSettingsControllerTests.java +++ b/server/src/test/java/org/elasticsearch/operator/OperatorClusterStateControllerTests.java @@ -25,7 +25,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -public class OperatorSettingsControllerTests extends ESTestCase { +public class OperatorClusterStateControllerTests extends ESTestCase { public void testOperatorController() throws IOException { ClusterSettings clusterSettings = new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS); @@ -35,7 +35,7 @@ public void testOperatorController() throws IOException { ClusterState state = ClusterState.builder(clusterName).build(); when(clusterService.state()).thenReturn(state); - OperatorSettingsController controller = new OperatorSettingsController(clusterService); + OperatorClusterStateController controller = new OperatorClusterStateController(clusterService); controller.initHandlers(List.of(new OperatorClusterUpdateSettingsAction(clusterSettings))); String testJSON = """ @@ -47,36 +47,12 @@ public void testOperatorController() throws IOException { "transient": { "cluster.routing.allocation.enable": "none" } - }, - "ilm": { - "my_timeseries_lifecycle": { - "policy": { - "phases": { - "warm": { - "min_age": "10s", - "actions": { - "forcemerge": { - "max_num_segments": 10000 - } - } - }, - "delete": { - "min_age": "30s", - "actions": { - "delete": { - - } - } - } - } - } - } } } """; try (XContentParser parser = XContentType.JSON.xContent().createParser(XContentParserConfiguration.EMPTY, testJSON)) { - controller.process(parser); + controller.process("operator", parser); } } } diff --git a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/action/operator/OperatorILMControllerTests.java b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/action/operator/OperatorILMControllerTests.java new file mode 100644 index 0000000000000..44fee7bc2c1ce --- /dev/null +++ b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/action/operator/OperatorILMControllerTests.java @@ -0,0 +1,153 @@ +/* + * 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.ilm.action.operator; + +import org.elasticsearch.client.internal.Client; +import org.elasticsearch.cluster.ClusterModule; +import org.elasticsearch.cluster.ClusterName; +import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.common.settings.ClusterSettings; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.license.XPackLicenseState; +import org.elasticsearch.operator.OperatorClusterStateController; +import org.elasticsearch.operator.action.OperatorClusterUpdateSettingsAction; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xcontent.NamedXContentRegistry; +import org.elasticsearch.xcontent.ParseField; +import org.elasticsearch.xcontent.XContentParser; +import org.elasticsearch.xcontent.XContentParserConfiguration; +import org.elasticsearch.xcontent.XContentType; +import org.elasticsearch.xpack.core.ilm.AllocateAction; +import org.elasticsearch.xpack.core.ilm.DeleteAction; +import org.elasticsearch.xpack.core.ilm.ForceMergeAction; +import org.elasticsearch.xpack.core.ilm.FreezeAction; +import org.elasticsearch.xpack.core.ilm.LifecycleAction; +import org.elasticsearch.xpack.core.ilm.LifecycleType; +import org.elasticsearch.xpack.core.ilm.MigrateAction; +import org.elasticsearch.xpack.core.ilm.ReadOnlyAction; +import org.elasticsearch.xpack.core.ilm.RolloverAction; +import org.elasticsearch.xpack.core.ilm.RollupILMAction; +import org.elasticsearch.xpack.core.ilm.SearchableSnapshotAction; +import org.elasticsearch.xpack.core.ilm.SetPriorityAction; +import org.elasticsearch.xpack.core.ilm.ShrinkAction; +import org.elasticsearch.xpack.core.ilm.TimeseriesLifecycleType; +import org.elasticsearch.xpack.core.ilm.UnfollowAction; +import org.elasticsearch.xpack.core.ilm.WaitForSnapshotAction; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class OperatorILMControllerTests extends ESTestCase { + + protected NamedXContentRegistry xContentRegistry() { + List entries = new ArrayList<>(ClusterModule.getNamedXWriteables()); + entries.addAll( + Arrays.asList( + new NamedXContentRegistry.Entry( + LifecycleType.class, + new ParseField(TimeseriesLifecycleType.TYPE), + (p) -> TimeseriesLifecycleType.INSTANCE + ), + new NamedXContentRegistry.Entry(LifecycleAction.class, new ParseField(AllocateAction.NAME), AllocateAction::parse), + new NamedXContentRegistry.Entry( + LifecycleAction.class, + new ParseField(WaitForSnapshotAction.NAME), + WaitForSnapshotAction::parse + ), + new NamedXContentRegistry.Entry( + LifecycleAction.class, + new ParseField(SearchableSnapshotAction.NAME), + SearchableSnapshotAction::parse + ), + new NamedXContentRegistry.Entry(LifecycleAction.class, new ParseField(DeleteAction.NAME), DeleteAction::parse), + new NamedXContentRegistry.Entry(LifecycleAction.class, new ParseField(ForceMergeAction.NAME), ForceMergeAction::parse), + new NamedXContentRegistry.Entry(LifecycleAction.class, new ParseField(ReadOnlyAction.NAME), ReadOnlyAction::parse), + new NamedXContentRegistry.Entry(LifecycleAction.class, new ParseField(RolloverAction.NAME), RolloverAction::parse), + new NamedXContentRegistry.Entry(LifecycleAction.class, new ParseField(ShrinkAction.NAME), ShrinkAction::parse), + new NamedXContentRegistry.Entry(LifecycleAction.class, new ParseField(FreezeAction.NAME), FreezeAction::parse), + new NamedXContentRegistry.Entry(LifecycleAction.class, new ParseField(SetPriorityAction.NAME), SetPriorityAction::parse), + new NamedXContentRegistry.Entry(LifecycleAction.class, new ParseField(MigrateAction.NAME), MigrateAction::parse), + new NamedXContentRegistry.Entry(LifecycleAction.class, new ParseField(UnfollowAction.NAME), UnfollowAction::parse), + new NamedXContentRegistry.Entry(LifecycleAction.class, new ParseField(RollupILMAction.NAME), RollupILMAction::parse) + ) + ); + return new NamedXContentRegistry(entries); + } + + public void testOperatorController() throws IOException { + ClusterSettings clusterSettings = new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS); + ClusterService clusterService = mock(ClusterService.class); + final ClusterName clusterName = new ClusterName("elasticsearch"); + + ClusterState state = ClusterState.builder(clusterName).build(); + when(clusterService.state()).thenReturn(state); + + OperatorClusterStateController controller = new OperatorClusterStateController(clusterService); + controller.initHandlers(List.of(new OperatorClusterUpdateSettingsAction(clusterSettings))); + + String testJSON = """ + { + "cluster": { + "persistent": { + "indices.recovery.max_bytes_per_sec": "50mb" + }, + "transient": { + "cluster.routing.allocation.enable": "none" + } + }, + "ilm": { + "my_timeseries_lifecycle": { + "policy": { + "phases": { + "warm": { + "min_age": "10s", + "actions": { + } + }, + "delete": { + "min_age": "30s", + "actions": { + } + } + } + } + } + } + } + """; + + try (XContentParser parser = XContentType.JSON.xContent().createParser(XContentParserConfiguration.EMPTY, testJSON)) { + assertEquals( + "Unknown settings definition type: ilm", + expectThrows(IllegalStateException.class, () -> controller.process("operator", parser)).getMessage() + ); + } + + Client client = mock(Client.class); + when(client.settings()).thenReturn(Settings.EMPTY); + + XPackLicenseState licenseState = mock(XPackLicenseState.class); + + controller.initHandlers( + List.of( + new OperatorClusterUpdateSettingsAction(clusterSettings), + new OperatorPutLifecycleAction(xContentRegistry(), client, licenseState) + ) + ); + + try (XContentParser parser = XContentType.JSON.xContent().createParser(XContentParserConfiguration.EMPTY, testJSON)) { + controller.process("operator", parser); + } + } +} From 682dec49069e4c0df1aac1b9733c59a1972d50d5 Mon Sep 17 00:00:00 2001 From: Nikola Grcevski Date: Tue, 26 Apr 2022 17:20:44 -0400 Subject: [PATCH 06/48] Fix merge --- .../TransportClusterUpdateSettingsAction.java | 90 +++++++++---------- .../action/TransportPutLifecycleAction.java | 62 +------------ 2 files changed, 46 insertions(+), 106 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/settings/TransportClusterUpdateSettingsAction.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/settings/TransportClusterUpdateSettingsAction.java index ed0bd88c34a07..2c278cc4f8f30 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/cluster/settings/TransportClusterUpdateSettingsAction.java +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/settings/TransportClusterUpdateSettingsAction.java @@ -146,53 +146,49 @@ protected void masterOperation( ) { final SettingsUpdater updater = new SettingsUpdater(clusterSettings); submitUnbatchedTask(UPDATE_TASK_SOURCE, new ClusterUpdateSettingsTask(clusterSettings, Priority.IMMEDIATE, request, listener) { - @Override - protected ClusterUpdateSettingsResponse newResponse(boolean acknowledged) { - return new ClusterUpdateSettingsResponse(acknowledged, updater.getTransientUpdates(), updater.getPersistentUpdate()); - } + @Override + protected ClusterUpdateSettingsResponse newResponse(boolean acknowledged) { + return new ClusterUpdateSettingsResponse(acknowledged, updater.getTransientUpdates(), updater.getPersistentUpdate()); + } - @Override - public void onAllNodesAcked() { - if (changed) { - reroute(true); - } else { - super.onAllNodesAcked(); - } + @Override + public void onAllNodesAcked() { + if (changed) { + reroute(true); + } else { + super.onAllNodesAcked(); } + } - @Override - public void onAckFailure(Exception e) { - if (changed) { - reroute(true); - } else { - super.onAckFailure(e); - } + @Override + public void onAckFailure(Exception e) { + if (changed) { + reroute(true); + } else { + super.onAckFailure(e); } + } - @Override - public void onAckTimeout() { - if (changed) { - reroute(false); - } else { - super.onAckTimeout(); - } + @Override + public void onAckTimeout() { + if (changed) { + reroute(false); + } else { + super.onAckTimeout(); } + } - private void reroute(final boolean updateSettingsAcked) { - // We're about to send a second update task, so we need to check if we're still the elected master - // For example the minimum_master_node could have been breached and we're no longer elected master, - // so we should *not* execute the reroute. - if (clusterService.state().nodes().isLocalNodeElectedMaster() == false) { - logger.debug("Skipping reroute after cluster update settings, because node is no longer master"); - listener.onResponse( - new ClusterUpdateSettingsResponse( - updateSettingsAcked, - updater.getTransientUpdates(), - updater.getPersistentUpdate() - ) - ); - return; - } + private void reroute(final boolean updateSettingsAcked) { + // We're about to send a second update task, so we need to check if we're still the elected master + // For example the minimum_master_node could have been breached and we're no longer elected master, + // so we should *not* execute the reroute. + if (clusterService.state().nodes().isLocalNodeElectedMaster() == false) { + logger.debug("Skipping reroute after cluster update settings, because node is no longer master"); + listener.onResponse( + new ClusterUpdateSettingsResponse(updateSettingsAcked, updater.getTransientUpdates(), updater.getPersistentUpdate()) + ); + return; + } // The reason the reroute needs to be send as separate update task, is that all the *cluster* settings are encapsulate // in the components (e.g. FilterAllocationDecider), so the changes made by the first call aren't visible @@ -244,14 +240,12 @@ public ClusterState execute(final ClusterState currentState) { }); } - @Override - public void onFailure(Exception e) { - logger.debug(() -> new ParameterizedMessage("failed to perform [{}]", UPDATE_TASK_SOURCE), e); - super.onFailure(e); - } - }, - newExecutor() - ); + @Override + public void onFailure(Exception e) { + logger.debug(() -> new ParameterizedMessage("failed to perform [{}]", UPDATE_TASK_SOURCE), e); + super.onFailure(e); + } + }); } public static class ClusterUpdateSettingsTask extends AckedClusterStateUpdateTask { diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportPutLifecycleAction.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportPutLifecycleAction.java index a0d309b851eba..0ed8d35713eaf 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportPutLifecycleAction.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportPutLifecycleAction.java @@ -111,64 +111,10 @@ protected void masterOperation(Task task, Request request, ClusterState state, A } } - submitUnbatchedTask("put-lifecycle-" + request.getPolicy().getName(), new AckedClusterStateUpdateTask(request, listener) { - @Override - public ClusterState execute(ClusterState currentState) throws Exception { - final IndexLifecycleMetadata currentMetadata = currentState.metadata() - .custom(IndexLifecycleMetadata.TYPE, IndexLifecycleMetadata.EMPTY); - final LifecyclePolicyMetadata existingPolicyMetadata = currentMetadata.getPolicyMetadatas() - .get(request.getPolicy().getName()); - - // Double-check for no-op in the state update task, in case it was changed/reset in the meantime - if (isNoopUpdate(existingPolicyMetadata, request.getPolicy(), filteredHeaders)) { - return currentState; - } - - validatePrerequisites(request.getPolicy(), currentState); - - ClusterState.Builder stateBuilder = ClusterState.builder(currentState); - long nextVersion = (existingPolicyMetadata == null) ? 1L : existingPolicyMetadata.getVersion() + 1L; - SortedMap newPolicies = new TreeMap<>(currentMetadata.getPolicyMetadatas()); - LifecyclePolicyMetadata lifecyclePolicyMetadata = new LifecyclePolicyMetadata( - request.getPolicy(), - filteredHeaders, - nextVersion, - Instant.now().toEpochMilli() - ); - LifecyclePolicyMetadata oldPolicy = newPolicies.put(lifecyclePolicyMetadata.getName(), lifecyclePolicyMetadata); - if (oldPolicy == null) { - logger.info("adding index lifecycle policy [{}]", request.getPolicy().getName()); - } else { - logger.info("updating index lifecycle policy [{}]", request.getPolicy().getName()); - } - IndexLifecycleMetadata newMetadata = new IndexLifecycleMetadata(newPolicies, currentMetadata.getOperationMode()); - stateBuilder.metadata( - Metadata.builder(currentState.getMetadata()).putCustom(IndexLifecycleMetadata.TYPE, newMetadata).build() - ); - ClusterState nonRefreshedState = stateBuilder.build(); - if (oldPolicy == null) { - return nonRefreshedState; - } else { - try { - return updateIndicesForPolicy( - nonRefreshedState, - xContentRegistry, - client, - oldPolicy.getPolicy(), - lifecyclePolicyMetadata, - licenseState - ); - } catch (Exception e) { - logger.warn( - new ParameterizedMessage("unable to refresh indices phase JSON for updated policy [{}]", oldPolicy.getName()), - e - ); - // Revert to the non-refreshed state - return nonRefreshedState; - } - } - } - }); + submitUnbatchedTask( + "put-lifecycle-" + request.getPolicy().getName(), + new UpdateLifecycleTask(request, listener, licenseState, filteredHeaders, xContentRegistry, client) + ); } public static class UpdateLifecycleTask extends AckedClusterStateUpdateTask { From 5db45de592104a75baf9a4534b87d7399469fd28 Mon Sep 17 00:00:00 2001 From: Nikola Grcevski Date: Thu, 28 Apr 2022 19:24:11 -0400 Subject: [PATCH 07/48] Add file watcher service --- .../java/org/elasticsearch/node/Node.java | 6 + .../operator/FileSettingsService.java | 177 ++++++++++++++++++ 2 files changed, 183 insertions(+) create mode 100644 server/src/main/java/org/elasticsearch/operator/FileSettingsService.java diff --git a/server/src/main/java/org/elasticsearch/node/Node.java b/server/src/main/java/org/elasticsearch/node/Node.java index 317f92dcb7636..50ed0108965e1 100644 --- a/server/src/main/java/org/elasticsearch/node/Node.java +++ b/server/src/main/java/org/elasticsearch/node/Node.java @@ -129,6 +129,7 @@ import org.elasticsearch.monitor.MonitorService; import org.elasticsearch.monitor.fs.FsHealthService; import org.elasticsearch.monitor.jvm.JvmInfo; +import org.elasticsearch.operator.FileSettingsService; import org.elasticsearch.persistent.PersistentTasksClusterService; import org.elasticsearch.persistent.PersistentTasksExecutor; import org.elasticsearch.persistent.PersistentTasksExecutorRegistry; @@ -990,6 +991,8 @@ protected Node( modules.add(b -> b.bind(ReadinessService.class).toInstance(new ReadinessService(clusterService, environment))); } + modules.add(b -> b.bind(FileSettingsService.class).toInstance(new FileSettingsService(clusterService, environment))); + injector = modules.createInjector(); // We allocate copies of existing shards by looking for a viable copy of the shard in the cluster and assigning the shard there. @@ -1135,6 +1138,7 @@ public Node start() throws NodeValidationException { if (ReadinessService.enabled(environment)) { injector.getInstance(ReadinessService.class).start(); } + injector.getInstance(FileSettingsService.class).start(); injector.getInstance(MappingUpdatedAction.class).setClient(client); injector.getInstance(IndicesService.class).start(); injector.getInstance(IndicesClusterStateService.class).start(); @@ -1283,6 +1287,7 @@ private Node stop() { if (ReadinessService.enabled(environment)) { injector.getInstance(ReadinessService.class).stop(); } + injector.getInstance(FileSettingsService.class).stop(); injector.getInstance(ResourceWatcherService.class).close(); injector.getInstance(HttpServerTransport.class).stop(); @@ -1366,6 +1371,7 @@ public synchronized void close() throws IOException { if (ReadinessService.enabled(environment)) { toClose.add(injector.getInstance(ReadinessService.class)); } + toClose.add(injector.getInstance(FileSettingsService.class)); for (LifecycleComponent plugin : pluginLifecycleComponents) { toClose.add(() -> stopWatch.stop().start("plugin(" + plugin.getClass().getName() + ")")); diff --git a/server/src/main/java/org/elasticsearch/operator/FileSettingsService.java b/server/src/main/java/org/elasticsearch/operator/FileSettingsService.java new file mode 100644 index 0000000000000..131c164dd13d4 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/operator/FileSettingsService.java @@ -0,0 +1,177 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.operator; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.cluster.ClusterChangedEvent; +import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.ClusterStateListener; +import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.common.component.AbstractLifecycleComponent; +import org.elasticsearch.common.settings.Setting; +import org.elasticsearch.core.PathUtils; +import org.elasticsearch.env.Environment; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardWatchEventKinds; +import java.nio.file.WatchKey; +import java.nio.file.WatchService; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.concurrent.CountDownLatch; + +public class FileSettingsService extends AbstractLifecycleComponent implements ClusterStateListener { + private static final Logger logger = LogManager.getLogger(FileSettingsService.class); + + private final ClusterService clusterService; + private final Environment environment; + + private WatchService watchService; // null; + private CountDownLatch watcherThreadLatch; + + private volatile long lastUpdatedTime = 0L; + + public static final Setting OPERATOR_SETTINGS = Setting.simpleString( + "readiness.port", + "operatorSettings.json", + Setting.Property.NodeScope + ); + + public FileSettingsService(ClusterService clusterService, Environment environment) { + this.clusterService = clusterService; + this.environment = environment; + clusterService.addListener(this); + } + + // package private for testing + Path operatorSettingsFile() { + String fileName = OPERATOR_SETTINGS.get(environment.settings()); + return environment.configFile().resolve(fileName); + } + + // package private for testing + static long watchedFileTimestamp(Path path) throws IOException { + if (Files.exists(path) == false) { + return 0; + } + BasicFileAttributes attr = Files.readAttributes(path, BasicFileAttributes.class); + + return attr.lastModifiedTime().toMillis(); + } + + @Override + // We start the file watcher when we know we are master + protected void doStart() {} + + @Override + protected void doStop() { + stopWatcher(); + } + + @Override + protected void doClose() {} + + @Override + public void clusterChanged(ClusterChangedEvent event) { + ClusterState clusterState = event.state(); + setWatching(clusterState.nodes().getMasterNodeId().equals(clusterState.nodes().getLocalNodeId())); + } + + private void setWatching(boolean watching) { + if (watching) { + startWatcher(); + } else { + stopWatcher(); + } + } + + // package private for testing + boolean watching() { + return this.watchService != null; + } + + synchronized void startWatcher() { + if (watching()) { + // already watching, nothing to do + return; + } + + Path path = operatorSettingsFile(); + try { + this.lastUpdatedTime = watchedFileTimestamp(path); + if (lastUpdatedTime > 0L) { + processFileSettings(path); + } + } catch (IOException e) { + logger.warn("Encountered I/O exception trying to read file attributes for the file based settings", e); + } + + try { + this.watchService = PathUtils.getDefaultFileSystem().newWatchService(); + } catch (IOException e) { + throw new IllegalStateException("Unable to launch a new watch service", e); + } + this.watcherThreadLatch = new CountDownLatch(1); + + new Thread(() -> { + try { + path.getParent() + .register( + watchService, + StandardWatchEventKinds.ENTRY_MODIFY, + StandardWatchEventKinds.ENTRY_CREATE, + StandardWatchEventKinds.ENTRY_DELETE + ); + + logger.info("File settings service up and running..."); + + WatchKey key; + while ((key = watchService.take()) != null) { + // Reading and interpreting watch service events can vary from platform to platform. + // After we get an indication that something has changed, we check the timestamp of our desired file. + try { + long updatedTime = watchedFileTimestamp(path); + if (updatedTime > lastUpdatedTime) { + this.lastUpdatedTime = updatedTime; + processFileSettings(path); + } + } catch (IOException e) { + logger.warn("Unable to read file attributes of " + path, e); + } + key.reset(); + } + } catch (InterruptedException | IOException e) { + logger.error("Encountered I/O error watching " + path, e); + } finally { + watcherThreadLatch.countDown(); + } + }, "elasticsearch[file-settings-watcher]").start(); + } + + synchronized void stopWatcher() { + if (watching()) { + try { + watchService.close(); + watcherThreadLatch.await(); + } catch (IOException | InterruptedException e) { + logger.info("Encountered exception while closing watch service", e); + } finally { + watchService = null; + logger.info("watcher service stopped"); + } + } + } + + void processFileSettings(Path path) { + // TODO: implement me + logger.info("Settings file changed event"); + } +} From fc45962fd6a20b5cdd4fe2735db1d7a46a85b705 Mon Sep 17 00:00:00 2001 From: Nikola Grcevski Date: Thu, 28 Apr 2022 20:18:53 -0400 Subject: [PATCH 08/48] Add additional exception handling --- .../java/org/elasticsearch/operator/FileSettingsService.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/org/elasticsearch/operator/FileSettingsService.java b/server/src/main/java/org/elasticsearch/operator/FileSettingsService.java index 131c164dd13d4..5b98b7d08fab2 100644 --- a/server/src/main/java/org/elasticsearch/operator/FileSettingsService.java +++ b/server/src/main/java/org/elasticsearch/operator/FileSettingsService.java @@ -20,6 +20,7 @@ import org.elasticsearch.env.Environment; import java.io.IOException; +import java.nio.file.ClosedWatchServiceException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardWatchEventKinds; @@ -148,7 +149,7 @@ synchronized void startWatcher() { } key.reset(); } - } catch (InterruptedException | IOException e) { + } catch (InterruptedException | IOException | ClosedWatchServiceException e) { logger.error("Encountered I/O error watching " + path, e); } finally { watcherThreadLatch.countDown(); From 273e56d6217acfbd5c69cda454dc871ce7d59f68 Mon Sep 17 00:00:00 2001 From: Nikola Grcevski Date: Fri, 29 Apr 2022 14:32:20 -0400 Subject: [PATCH 09/48] Fix issue with watcher starting after service is stopped --- .../operator/FileSettingsService.java | 67 +++++++++++++------ 1 file changed, 47 insertions(+), 20 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/operator/FileSettingsService.java b/server/src/main/java/org/elasticsearch/operator/FileSettingsService.java index 5b98b7d08fab2..acb513e564ea3 100644 --- a/server/src/main/java/org/elasticsearch/operator/FileSettingsService.java +++ b/server/src/main/java/org/elasticsearch/operator/FileSettingsService.java @@ -40,6 +40,8 @@ public class FileSettingsService extends AbstractLifecycleComponent implements C private volatile long lastUpdatedTime = 0L; + private volatile boolean active = false; + public static final Setting OPERATOR_SETTINGS = Setting.simpleString( "readiness.port", "operatorSettings.json", @@ -69,11 +71,17 @@ static long watchedFileTimestamp(Path path) throws IOException { } @Override - // We start the file watcher when we know we are master - protected void doStart() {} + protected void doStart() { + // We start the file watcher when we know we are master. + // We need this additional flag, since cluster state can change after we've shutdown the service + // causing the watcher to start again. + this.active = true; + } @Override protected void doStop() { + this.active = false; + logger.debug("Stopping file settings service"); stopWatcher(); } @@ -100,39 +108,55 @@ boolean watching() { } synchronized void startWatcher() { - if (watching()) { - // already watching, nothing to do + if (watching() || active == false) { + // already watching or inactive, nothing to do return; } + logger.info("starting file settings watcher ..."); + Path path = operatorSettingsFile(); + Path configDir = path.getParent(); + + if (Files.exists(configDir) == false) { + logger.warn("file based settings service disabled because config dir [{}] doesn't exist", configDir); + return; + } + try { this.lastUpdatedTime = watchedFileTimestamp(path); if (lastUpdatedTime > 0L) { processFileSettings(path); } } catch (IOException e) { - logger.warn("Encountered I/O exception trying to read file attributes for the file based settings", e); + logger.warn("encountered I/O exception trying to read file attributes for the file based settings", e); } try { this.watchService = PathUtils.getDefaultFileSystem().newWatchService(); - } catch (IOException e) { - throw new IllegalStateException("Unable to launch a new watch service", e); + configDir.register( + watchService, + StandardWatchEventKinds.ENTRY_MODIFY, + StandardWatchEventKinds.ENTRY_CREATE, + StandardWatchEventKinds.ENTRY_DELETE + ); + } catch (Exception e) { + if (watchService != null) { + try { + this.watchService.close(); + } catch (Exception ignore) {} finally { + this.watchService = null; + } + } + + throw new IllegalStateException("unable to launch a new watch service", e); } + this.watcherThreadLatch = new CountDownLatch(1); new Thread(() -> { try { - path.getParent() - .register( - watchService, - StandardWatchEventKinds.ENTRY_MODIFY, - StandardWatchEventKinds.ENTRY_CREATE, - StandardWatchEventKinds.ENTRY_DELETE - ); - - logger.info("File settings service up and running..."); + logger.info("file settings service up and running [tid={}]", Thread.currentThread().getId()); WatchKey key; while ((key = watchService.take()) != null) { @@ -145,12 +169,12 @@ synchronized void startWatcher() { processFileSettings(path); } } catch (IOException e) { - logger.warn("Unable to read file attributes of " + path, e); + logger.warn("unable to read file attributes of " + path, e); } key.reset(); } - } catch (InterruptedException | IOException | ClosedWatchServiceException e) { - logger.error("Encountered I/O error watching " + path, e); + } catch (InterruptedException | ClosedWatchServiceException e) { + logger.debug("encountered exception watching. Shutting down watcher thread.", e); } finally { watcherThreadLatch.countDown(); } @@ -158,6 +182,7 @@ synchronized void startWatcher() { } synchronized void stopWatcher() { + logger.debug("stopping watcher ..."); if (watching()) { try { watchService.close(); @@ -168,11 +193,13 @@ synchronized void stopWatcher() { watchService = null; logger.info("watcher service stopped"); } + } else { + logger.debug("file settings service already stopped"); } } void processFileSettings(Path path) { // TODO: implement me - logger.info("Settings file changed event"); + logger.info("settings file changed event"); } } From 253344844ed2f9459a724ad36a75aee8ef3f86d9 Mon Sep 17 00:00:00 2001 From: Nikola Grcevski Date: Fri, 29 Apr 2022 16:09:39 -0400 Subject: [PATCH 10/48] Add delete paths --- .../threadpool/SimpleThreadPoolIT.java | 3 +- .../OperatorClusterStateController.java | 4 +- .../operator/OperatorHandler.java | 3 +- .../operator/TransformState.java | 18 +++++ .../OperatorClusterUpdateSettingsAction.java | 34 ++++++++-- .../xpack/ilm/IndexLifecycle.java | 6 +- .../TransportDeleteLifecycleAction.java | 67 +++++++++++-------- .../action/TransportPutLifecycleAction.java | 13 ++-- ...tion.java => OperatorLifecycleAction.java} | 30 +++++++-- .../operator/OperatorILMControllerTests.java | 2 +- 10 files changed, 130 insertions(+), 50 deletions(-) create mode 100644 server/src/main/java/org/elasticsearch/operator/TransformState.java rename x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/operator/{OperatorPutLifecycleAction.java => OperatorLifecycleAction.java} (64%) diff --git a/server/src/internalClusterTest/java/org/elasticsearch/threadpool/SimpleThreadPoolIT.java b/server/src/internalClusterTest/java/org/elasticsearch/threadpool/SimpleThreadPoolIT.java index e5c676da7a374..05d44003c4ff7 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/threadpool/SimpleThreadPoolIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/threadpool/SimpleThreadPoolIT.java @@ -81,7 +81,8 @@ public void testThreadNames() throws Exception { // or the ones that are occasionally come up from ESSingleNodeTestCase if (threadName.contains("[node_s_0]") // TODO: this can't possibly be right! single node and integ test are unrelated! || threadName.contains("Keep-Alive-Timer") - || threadName.contains("readiness-service")) { + || threadName.contains("readiness-service") + || threadName.contains("FileSystemWatchService")) { continue; } String nodePrefix = "(" diff --git a/server/src/main/java/org/elasticsearch/operator/OperatorClusterStateController.java b/server/src/main/java/org/elasticsearch/operator/OperatorClusterStateController.java index 34c249a6124a3..23054cbd14284 100644 --- a/server/src/main/java/org/elasticsearch/operator/OperatorClusterStateController.java +++ b/server/src/main/java/org/elasticsearch/operator/OperatorClusterStateController.java @@ -13,6 +13,7 @@ import org.elasticsearch.xcontent.XContentParser; import java.io.IOException; +import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -47,7 +48,8 @@ public ClusterState process(String namespace, XContentParser parser) throws IOEx for (var handlerKey : orderedHandlers) { OperatorHandler handler = handlers.get(handlerKey); try { - state = handler.transform(source.get(handlerKey), state); + // TODO: fetch and pass previous keys for handler to be able to delete + state = handler.transform(source.get(handlerKey), new TransformState(state, new HashSet<>())).state(); } catch (Exception e) { throw new IllegalStateException("Error processing state change request for: " + handler.key(), e); } diff --git a/server/src/main/java/org/elasticsearch/operator/OperatorHandler.java b/server/src/main/java/org/elasticsearch/operator/OperatorHandler.java index 7a41b026b0337..bc25eb790a1a8 100644 --- a/server/src/main/java/org/elasticsearch/operator/OperatorHandler.java +++ b/server/src/main/java/org/elasticsearch/operator/OperatorHandler.java @@ -11,7 +11,6 @@ import org.elasticsearch.ElasticsearchGenerationException; import org.elasticsearch.action.ActionRequestValidationException; import org.elasticsearch.action.support.master.MasterNodeRequest; -import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.common.Strings; import org.elasticsearch.xcontent.XContentBuilder; import org.elasticsearch.xcontent.XContentFactory; @@ -32,7 +31,7 @@ public interface OperatorHandler> { String key(); - ClusterState transform(Object source, ClusterState state) throws Exception; + TransformState transform(Object source, TransformState prevState) throws Exception; default Collection dependencies() { return Collections.emptyList(); diff --git a/server/src/main/java/org/elasticsearch/operator/TransformState.java b/server/src/main/java/org/elasticsearch/operator/TransformState.java new file mode 100644 index 0000000000000..71b0a3db66407 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/operator/TransformState.java @@ -0,0 +1,18 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.operator; + +import org.elasticsearch.cluster.ClusterState; + +import java.util.Set; + +/** + * TODO: Add docs + */ +public record TransformState(ClusterState state, Set keys) {} diff --git a/server/src/main/java/org/elasticsearch/operator/action/OperatorClusterUpdateSettingsAction.java b/server/src/main/java/org/elasticsearch/operator/action/OperatorClusterUpdateSettingsAction.java index d5be5ddcaf98c..21fb31e6ff09b 100644 --- a/server/src/main/java/org/elasticsearch/operator/action/OperatorClusterUpdateSettingsAction.java +++ b/server/src/main/java/org/elasticsearch/operator/action/OperatorClusterUpdateSettingsAction.java @@ -14,8 +14,13 @@ import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.common.settings.ClusterSettings; import org.elasticsearch.operator.OperatorHandler; +import org.elasticsearch.operator.TransformState; +import java.util.HashMap; +import java.util.HashSet; import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; import static org.elasticsearch.rest.action.admin.cluster.RestClusterUpdateSettingsAction.PERSISTENT; @@ -38,26 +43,43 @@ public String key() { } @SuppressWarnings("unchecked") - private ClusterUpdateSettingsRequest prepare(Object input) { + private ClusterUpdateSettingsRequest prepare(Object input, Set previouslySet) { final ClusterUpdateSettingsRequest clusterUpdateSettingsRequest = Requests.clusterUpdateSettingsRequest(); Map source = asMap(input); + Map persistentSettings = new HashMap<>(); + Set toDelete = new HashSet<>(previouslySet); if (source.containsKey(PERSISTENT)) { - clusterUpdateSettingsRequest.persistentSettings((Map) source.get(PERSISTENT)); + ((Map) source.get(PERSISTENT)).forEach((k, v) -> { + persistentSettings.put(k, v); + toDelete.remove(k); + }); } + toDelete.forEach(k -> persistentSettings.put(k, null)); + + clusterUpdateSettingsRequest.persistentSettings(persistentSettings); return clusterUpdateSettingsRequest; } @Override - public ClusterState transform(Object input, ClusterState state) { - - ClusterUpdateSettingsRequest request = prepare(input); + public TransformState transform(Object input, TransformState prevState) { + ClusterUpdateSettingsRequest request = prepare(input, prevState.keys()); validate(request); + ClusterState state = prevState.state(); + TransportClusterUpdateSettingsAction.ClusterUpdateSettingsTask updateSettingsTask = new TransportClusterUpdateSettingsAction.ClusterUpdateSettingsTask(clusterSettings, request); - return updateSettingsTask.execute(state); + + state = updateSettingsTask.execute(state); + Set currentKeys = request.persistentSettings() + .keySet() + .stream() + .filter(k -> request.persistentSettings().hasValue(k)) + .collect(Collectors.toSet()); + + return new TransformState(state, currentKeys); } } diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycle.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycle.java index f4ef60383e085..9194794d66aed 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycle.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycle.java @@ -106,7 +106,7 @@ import org.elasticsearch.xpack.ilm.action.TransportRetryAction; import org.elasticsearch.xpack.ilm.action.TransportStartILMAction; import org.elasticsearch.xpack.ilm.action.TransportStopILMAction; -import org.elasticsearch.xpack.ilm.action.operator.OperatorPutLifecycleAction; +import org.elasticsearch.xpack.ilm.action.operator.OperatorLifecycleAction; import org.elasticsearch.xpack.ilm.history.ILMHistoryStore; import org.elasticsearch.xpack.ilm.history.ILMHistoryTemplateRegistry; import org.elasticsearch.xpack.slm.SLMInfoTransportAction; @@ -160,7 +160,7 @@ public class IndexLifecycle extends Plugin implements ActionPlugin, HealthPlugin private final SetOnce snapshotHistoryStore = new SetOnce<>(); private final SetOnce ilmHealthIndicatorService = new SetOnce<>(); private final SetOnce slmHealthIndicatorService = new SetOnce<>(); - private final SetOnce ilmOperatorAction = new SetOnce<>(); + private final SetOnce ilmOperatorAction = new SetOnce<>(); private final Settings settings; public IndexLifecycle(Settings settings) { @@ -274,7 +274,7 @@ public Collection createComponents( components.addAll(Arrays.asList(snapshotLifecycleService.get(), snapshotHistoryStore.get(), snapshotRetentionService.get())); ilmHealthIndicatorService.set(new IlmHealthIndicatorService(clusterService)); slmHealthIndicatorService.set(new SlmHealthIndicatorService(clusterService)); - ilmOperatorAction.set(new OperatorPutLifecycleAction(xContentRegistry, client, XPackPlugin.getSharedLicenseState())); + ilmOperatorAction.set(new OperatorLifecycleAction(xContentRegistry, client, XPackPlugin.getSharedLicenseState())); return components; } diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportDeleteLifecycleAction.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportDeleteLifecycleAction.java index 458cd3e183927..4480d6571a8c9 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportDeleteLifecycleAction.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportDeleteLifecycleAction.java @@ -59,34 +59,47 @@ public TransportDeleteLifecycleAction( @Override protected void masterOperation(Task task, Request request, ClusterState state, ActionListener listener) { - submitUnbatchedTask("delete-lifecycle-" + request.getPolicyName(), new AckedClusterStateUpdateTask(request, listener) { - @Override - public ClusterState execute(ClusterState currentState) { - String policyToDelete = request.getPolicyName(); - List indicesUsingPolicy = currentState.metadata() - .indices() - .values() - .stream() - .filter(idxMeta -> policyToDelete.equals(idxMeta.getLifecyclePolicyName())) - .map(idxMeta -> idxMeta.getIndex().getName()) - .toList(); - if (indicesUsingPolicy.isEmpty() == false) { - throw new IllegalArgumentException( - "Cannot delete policy [" + request.getPolicyName() + "]. It is in use by one or more indices: " + indicesUsingPolicy - ); - } - ClusterState.Builder newState = ClusterState.builder(currentState); - IndexLifecycleMetadata currentMetadata = currentState.metadata().custom(IndexLifecycleMetadata.TYPE); - if (currentMetadata == null || currentMetadata.getPolicyMetadatas().containsKey(request.getPolicyName()) == false) { - throw new ResourceNotFoundException("Lifecycle policy not found: {}", request.getPolicyName()); - } - SortedMap newPolicies = new TreeMap<>(currentMetadata.getPolicyMetadatas()); - newPolicies.remove(request.getPolicyName()); - IndexLifecycleMetadata newMetadata = new IndexLifecycleMetadata(newPolicies, currentMetadata.getOperationMode()); - newState.metadata(Metadata.builder(currentState.getMetadata()).putCustom(IndexLifecycleMetadata.TYPE, newMetadata).build()); - return newState.build(); + submitUnbatchedTask("delete-lifecycle-" + request.getPolicyName(), new DeleteLifecyclePolicyTask(request, listener)); + } + + public static class DeleteLifecyclePolicyTask extends AckedClusterStateUpdateTask { + private final Request request; + + public DeleteLifecyclePolicyTask(Request request, ActionListener listener) { + super(request, listener); + this.request = request; + } + + public DeleteLifecyclePolicyTask(String policyName) { + this(new Request(policyName), null); + } + + @Override + public ClusterState execute(ClusterState currentState) { + String policyToDelete = request.getPolicyName(); + List indicesUsingPolicy = currentState.metadata() + .indices() + .values() + .stream() + .filter(idxMeta -> policyToDelete.equals(idxMeta.getLifecyclePolicyName())) + .map(idxMeta -> idxMeta.getIndex().getName()) + .toList(); + if (indicesUsingPolicy.isEmpty() == false) { + throw new IllegalArgumentException( + "Cannot delete policy [" + request.getPolicyName() + "]. It is in use by one or more indices: " + indicesUsingPolicy + ); + } + ClusterState.Builder newState = ClusterState.builder(currentState); + IndexLifecycleMetadata currentMetadata = currentState.metadata().custom(IndexLifecycleMetadata.TYPE); + if (currentMetadata == null || currentMetadata.getPolicyMetadatas().containsKey(request.getPolicyName()) == false) { + throw new ResourceNotFoundException("Lifecycle policy not found: {}", request.getPolicyName()); } - }); + SortedMap newPolicies = new TreeMap<>(currentMetadata.getPolicyMetadatas()); + newPolicies.remove(request.getPolicyName()); + IndexLifecycleMetadata newMetadata = new IndexLifecycleMetadata(newPolicies, currentMetadata.getOperationMode()); + newState.metadata(Metadata.builder(currentState.getMetadata()).putCustom(IndexLifecycleMetadata.TYPE, newMetadata).build()); + return newState.build(); + } } @SuppressForbidden(reason = "legacy usage of unbatched task") // TODO add support for batching here diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportPutLifecycleAction.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportPutLifecycleAction.java index 0ed8d35713eaf..7e1116a6fce13 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportPutLifecycleAction.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportPutLifecycleAction.java @@ -113,18 +113,18 @@ protected void masterOperation(Task task, Request request, ClusterState state, A submitUnbatchedTask( "put-lifecycle-" + request.getPolicy().getName(), - new UpdateLifecycleTask(request, listener, licenseState, filteredHeaders, xContentRegistry, client) + new UpdateLifecyclePolicyTask(request, listener, licenseState, filteredHeaders, xContentRegistry, client) ); } - public static class UpdateLifecycleTask extends AckedClusterStateUpdateTask { + public static class UpdateLifecyclePolicyTask extends AckedClusterStateUpdateTask { private final Request request; private final XPackLicenseState licenseState; private final Map filteredHeaders; private final NamedXContentRegistry xContentRegistry; private final Client client; - public UpdateLifecycleTask( + public UpdateLifecyclePolicyTask( Request request, ActionListener listener, XPackLicenseState licenseState, @@ -140,7 +140,12 @@ public UpdateLifecycleTask( this.client = client; } - public UpdateLifecycleTask(Request request, XPackLicenseState licenseState, NamedXContentRegistry xContentRegistry, Client client) { + public UpdateLifecyclePolicyTask( + Request request, + XPackLicenseState licenseState, + NamedXContentRegistry xContentRegistry, + Client client + ) { this(request, null, licenseState, new HashMap<>(), xContentRegistry, client); } diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/operator/OperatorPutLifecycleAction.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/operator/OperatorLifecycleAction.java similarity index 64% rename from x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/operator/OperatorPutLifecycleAction.java rename to x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/operator/OperatorLifecycleAction.java index 4c133255bcd3d..7b7c15f249ade 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/operator/OperatorPutLifecycleAction.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/operator/OperatorLifecycleAction.java @@ -11,21 +11,26 @@ import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.operator.OperatorHandler; +import org.elasticsearch.operator.TransformState; import org.elasticsearch.xcontent.NamedXContentRegistry; import org.elasticsearch.xcontent.XContentParser; import org.elasticsearch.xpack.core.ilm.action.PutLifecycleAction; +import org.elasticsearch.xpack.ilm.action.TransportDeleteLifecycleAction; import org.elasticsearch.xpack.ilm.action.TransportPutLifecycleAction; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; /** * TODO: Add docs */ -public class OperatorPutLifecycleAction implements OperatorHandler { +public class OperatorLifecycleAction implements OperatorHandler { private final NamedXContentRegistry xContentRegistry; private final Client client; @@ -33,7 +38,7 @@ public class OperatorPutLifecycleAction implements OperatorHandler prepare(Object input) throws IOExc } @Override - public ClusterState transform(Object source, ClusterState state) throws Exception { + public TransformState transform(Object source, TransformState prevState) throws Exception { var requests = prepare(source); + ClusterState state = prevState.state(); + for (var request : requests) { - TransportPutLifecycleAction.UpdateLifecycleTask task = new TransportPutLifecycleAction.UpdateLifecycleTask( + TransportPutLifecycleAction.UpdateLifecyclePolicyTask task = new TransportPutLifecycleAction.UpdateLifecyclePolicyTask( request, licenseState, xContentRegistry, @@ -76,6 +83,19 @@ public ClusterState transform(Object source, ClusterState state) throws Exceptio state = task.execute(state); } - return state; + + Set entities = requests.stream().map(r -> r.getPolicy().getName()).collect(Collectors.toSet()); + + Set toDelete = new HashSet<>(prevState.keys()); + toDelete.removeAll(entities); + + for (var policyToDelete : toDelete) { + TransportDeleteLifecycleAction.DeleteLifecyclePolicyTask task = new TransportDeleteLifecycleAction.DeleteLifecyclePolicyTask( + policyToDelete + ); + state = task.execute(state); + } + + return new TransformState(state, entities); } } diff --git a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/action/operator/OperatorILMControllerTests.java b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/action/operator/OperatorILMControllerTests.java index 44fee7bc2c1ce..9bf4a606f37a8 100644 --- a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/action/operator/OperatorILMControllerTests.java +++ b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/action/operator/OperatorILMControllerTests.java @@ -142,7 +142,7 @@ public void testOperatorController() throws IOException { controller.initHandlers( List.of( new OperatorClusterUpdateSettingsAction(clusterSettings), - new OperatorPutLifecycleAction(xContentRegistry(), client, licenseState) + new OperatorLifecycleAction(xContentRegistry(), client, licenseState) ) ); From 86ec3b456cd30c28eda8ea13ddd26f9485790146 Mon Sep 17 00:00:00 2001 From: Nikola Grcevski Date: Sat, 30 Apr 2022 16:35:01 -0400 Subject: [PATCH 11/48] One more thread to skip --- .../java/org/elasticsearch/threadpool/SimpleThreadPoolIT.java | 1 + 1 file changed, 1 insertion(+) diff --git a/server/src/internalClusterTest/java/org/elasticsearch/threadpool/SimpleThreadPoolIT.java b/server/src/internalClusterTest/java/org/elasticsearch/threadpool/SimpleThreadPoolIT.java index 05d44003c4ff7..c41cc80c953d6 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/threadpool/SimpleThreadPoolIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/threadpool/SimpleThreadPoolIT.java @@ -82,6 +82,7 @@ public void testThreadNames() throws Exception { if (threadName.contains("[node_s_0]") // TODO: this can't possibly be right! single node and integ test are unrelated! || threadName.contains("Keep-Alive-Timer") || threadName.contains("readiness-service") + || threadName.contains("file-settings-watcher") || threadName.contains("FileSystemWatchService")) { continue; } From dc21c399d537e45113e70c7416b76039f164a1e5 Mon Sep 17 00:00:00 2001 From: Nikola Grcevski Date: Thu, 26 May 2022 14:19:14 -0400 Subject: [PATCH 12/48] Refactor for modules --- .../elasticsearch/operator/{ => service}/FileSettingsService.java | 0 .../operator/{ => service}/OperatorClusterStateController.java | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename server/src/main/java/org/elasticsearch/operator/{ => service}/FileSettingsService.java (100%) rename server/src/main/java/org/elasticsearch/operator/{ => service}/OperatorClusterStateController.java (100%) diff --git a/server/src/main/java/org/elasticsearch/operator/FileSettingsService.java b/server/src/main/java/org/elasticsearch/operator/service/FileSettingsService.java similarity index 100% rename from server/src/main/java/org/elasticsearch/operator/FileSettingsService.java rename to server/src/main/java/org/elasticsearch/operator/service/FileSettingsService.java diff --git a/server/src/main/java/org/elasticsearch/operator/OperatorClusterStateController.java b/server/src/main/java/org/elasticsearch/operator/service/OperatorClusterStateController.java similarity index 100% rename from server/src/main/java/org/elasticsearch/operator/OperatorClusterStateController.java rename to server/src/main/java/org/elasticsearch/operator/service/OperatorClusterStateController.java From ae19af72895e94b2249c3900369d92822f69f909 Mon Sep 17 00:00:00 2001 From: Nikola Grcevski Date: Thu, 26 May 2022 14:19:26 -0400 Subject: [PATCH 13/48] Refactor for modules --- server/src/main/java/module-info.java | 1 + .../src/main/java/org/elasticsearch/action/ActionModule.java | 2 +- server/src/main/java/org/elasticsearch/node/Node.java | 2 +- .../elasticsearch/operator/service/FileSettingsService.java | 2 +- .../operator/service/OperatorClusterStateController.java | 4 +++- .../operator/OperatorClusterStateControllerTests.java | 1 + .../xpack/ilm/action/operator/OperatorILMControllerTests.java | 2 +- 7 files changed, 9 insertions(+), 5 deletions(-) diff --git a/server/src/main/java/module-info.java b/server/src/main/java/module-info.java index eb81804c58308..b1c2bcf4b4012 100644 --- a/server/src/main/java/module-info.java +++ b/server/src/main/java/module-info.java @@ -273,6 +273,7 @@ exports org.elasticsearch.monitor.os; exports org.elasticsearch.monitor.process; exports org.elasticsearch.node; + exports org.elasticsearch.operator; exports org.elasticsearch.persistent; exports org.elasticsearch.persistent.decider; exports org.elasticsearch.plugins; diff --git a/server/src/main/java/org/elasticsearch/action/ActionModule.java b/server/src/main/java/org/elasticsearch/action/ActionModule.java index 796a7a7e6d9d9..24c0df4bf2689 100644 --- a/server/src/main/java/org/elasticsearch/action/ActionModule.java +++ b/server/src/main/java/org/elasticsearch/action/ActionModule.java @@ -266,7 +266,7 @@ import org.elasticsearch.indices.SystemIndices; import org.elasticsearch.indices.breaker.CircuitBreakerService; import org.elasticsearch.indices.store.TransportNodesListShardStoreMetadata; -import org.elasticsearch.operator.OperatorClusterStateController; +import org.elasticsearch.operator.service.OperatorClusterStateController; import org.elasticsearch.operator.OperatorHandler; import org.elasticsearch.operator.action.OperatorClusterUpdateSettingsAction; import org.elasticsearch.persistent.CompletionPersistentTaskAction; diff --git a/server/src/main/java/org/elasticsearch/node/Node.java b/server/src/main/java/org/elasticsearch/node/Node.java index 3eebe80b36ff9..4bbadc1b5a566 100644 --- a/server/src/main/java/org/elasticsearch/node/Node.java +++ b/server/src/main/java/org/elasticsearch/node/Node.java @@ -131,7 +131,7 @@ import org.elasticsearch.monitor.MonitorService; import org.elasticsearch.monitor.fs.FsHealthService; import org.elasticsearch.monitor.jvm.JvmInfo; -import org.elasticsearch.operator.FileSettingsService; +import org.elasticsearch.operator.service.FileSettingsService; import org.elasticsearch.persistent.PersistentTasksClusterService; import org.elasticsearch.persistent.PersistentTasksExecutor; import org.elasticsearch.persistent.PersistentTasksExecutorRegistry; diff --git a/server/src/main/java/org/elasticsearch/operator/service/FileSettingsService.java b/server/src/main/java/org/elasticsearch/operator/service/FileSettingsService.java index acb513e564ea3..3c7a3c89781ff 100644 --- a/server/src/main/java/org/elasticsearch/operator/service/FileSettingsService.java +++ b/server/src/main/java/org/elasticsearch/operator/service/FileSettingsService.java @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -package org.elasticsearch.operator; +package org.elasticsearch.operator.service; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; diff --git a/server/src/main/java/org/elasticsearch/operator/service/OperatorClusterStateController.java b/server/src/main/java/org/elasticsearch/operator/service/OperatorClusterStateController.java index 23054cbd14284..385de7ffcf56b 100644 --- a/server/src/main/java/org/elasticsearch/operator/service/OperatorClusterStateController.java +++ b/server/src/main/java/org/elasticsearch/operator/service/OperatorClusterStateController.java @@ -6,10 +6,12 @@ * Side Public License, v 1. */ -package org.elasticsearch.operator; +package org.elasticsearch.operator.service; import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.operator.OperatorHandler; +import org.elasticsearch.operator.TransformState; import org.elasticsearch.xcontent.XContentParser; import java.io.IOException; diff --git a/server/src/test/java/org/elasticsearch/operator/OperatorClusterStateControllerTests.java b/server/src/test/java/org/elasticsearch/operator/OperatorClusterStateControllerTests.java index c8c39f515deb0..0fe82fdf4ebea 100644 --- a/server/src/test/java/org/elasticsearch/operator/OperatorClusterStateControllerTests.java +++ b/server/src/test/java/org/elasticsearch/operator/OperatorClusterStateControllerTests.java @@ -14,6 +14,7 @@ import org.elasticsearch.common.settings.ClusterSettings; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.operator.action.OperatorClusterUpdateSettingsAction; +import org.elasticsearch.operator.service.OperatorClusterStateController; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.xcontent.XContentParser; import org.elasticsearch.xcontent.XContentParserConfiguration; diff --git a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/action/operator/OperatorILMControllerTests.java b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/action/operator/OperatorILMControllerTests.java index 9bf4a606f37a8..f4c5ee5d4e210 100644 --- a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/action/operator/OperatorILMControllerTests.java +++ b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/action/operator/OperatorILMControllerTests.java @@ -15,7 +15,7 @@ import org.elasticsearch.common.settings.ClusterSettings; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.license.XPackLicenseState; -import org.elasticsearch.operator.OperatorClusterStateController; +import org.elasticsearch.operator.service.OperatorClusterStateController; import org.elasticsearch.operator.action.OperatorClusterUpdateSettingsAction; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.xcontent.NamedXContentRegistry; From 59b18cef645deed21d621b8972ad8dce94ebccf1 Mon Sep 17 00:00:00 2001 From: Nikola Grcevski Date: Mon, 30 May 2022 16:08:00 -0400 Subject: [PATCH 14/48] Add additional metadata to cluster state --- .../elasticsearch/action/ActionModule.java | 2 +- .../cluster/metadata/Metadata.java | 62 ++++++- .../metadata/OperatorHandlerMetadata.java | 129 +++++++++++++++ .../cluster/metadata/OperatorMetadata.java | 155 ++++++++++++++++++ .../OperatorClusterUpdateSettingsAction.java | 14 +- .../OperatorClusterStateController.java | 83 +++++++++- .../service/OperatorStateVersionMetadata.java | 57 +++++++ .../operator/OperatorILMControllerTests.java | 58 +++---- 8 files changed, 509 insertions(+), 51 deletions(-) create mode 100644 server/src/main/java/org/elasticsearch/cluster/metadata/OperatorHandlerMetadata.java create mode 100644 server/src/main/java/org/elasticsearch/cluster/metadata/OperatorMetadata.java create mode 100644 server/src/main/java/org/elasticsearch/operator/service/OperatorStateVersionMetadata.java diff --git a/server/src/main/java/org/elasticsearch/action/ActionModule.java b/server/src/main/java/org/elasticsearch/action/ActionModule.java index 24c0df4bf2689..93231676f0226 100644 --- a/server/src/main/java/org/elasticsearch/action/ActionModule.java +++ b/server/src/main/java/org/elasticsearch/action/ActionModule.java @@ -266,9 +266,9 @@ import org.elasticsearch.indices.SystemIndices; import org.elasticsearch.indices.breaker.CircuitBreakerService; import org.elasticsearch.indices.store.TransportNodesListShardStoreMetadata; -import org.elasticsearch.operator.service.OperatorClusterStateController; import org.elasticsearch.operator.OperatorHandler; import org.elasticsearch.operator.action.OperatorClusterUpdateSettingsAction; +import org.elasticsearch.operator.service.OperatorClusterStateController; import org.elasticsearch.persistent.CompletionPersistentTaskAction; import org.elasticsearch.persistent.RemovePersistentTaskAction; import org.elasticsearch.persistent.StartPersistentTaskAction; diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/Metadata.java b/server/src/main/java/org/elasticsearch/cluster/metadata/Metadata.java index 8c7854c3f398c..45ef0351e82a4 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/Metadata.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/Metadata.java @@ -207,6 +207,7 @@ default boolean isRestorable() { private final ImmutableOpenMap> aliasedIndices; private final ImmutableOpenMap templates; private final ImmutableOpenMap customs; + private final ImmutableOpenMap operatorState; private final transient int totalNumberOfShards; // Transient ? not serializable anyway? private final int totalOpenIndexShards; @@ -246,7 +247,8 @@ private Metadata( String[] visibleClosedIndices, SortedMap indicesLookup, Map mappingsByHash, - Version oldestIndexVersion + Version oldestIndexVersion, + ImmutableOpenMap operatorState ) { this.clusterUUID = clusterUUID; this.clusterUUIDCommitted = clusterUUIDCommitted; @@ -271,6 +273,7 @@ private Metadata( this.indicesLookup = indicesLookup; this.mappingsByHash = mappingsByHash; this.oldestIndexVersion = oldestIndexVersion; + this.operatorState = operatorState; } public Metadata withIncrementedVersion() { @@ -297,7 +300,8 @@ public Metadata withIncrementedVersion() { visibleClosedIndices, indicesLookup, mappingsByHash, - oldestIndexVersion + oldestIndexVersion, + operatorState ); } @@ -357,7 +361,8 @@ public Metadata withLifecycleState(final Index index, final LifecycleExecutionSt visibleClosedIndices, indicesLookup, mappingsByHash, - oldestIndexVersion + oldestIndexVersion, + operatorState ); } @@ -938,6 +943,14 @@ public Map customs() { return this.customs; } + public Map operatorState() { + return this.operatorState; + } + + public OperatorMetadata operatorState(String namespace) { + return this.operatorState.get(namespace); + } + /** * The collection of index deletions in the cluster. */ @@ -1079,6 +1092,7 @@ private static class MetadataDiff implements Diff { private final Diff> indices; private final Diff> templates; private final Diff> customs; + private final Diff> operatorState; MetadataDiff(Metadata before, Metadata after) { clusterUUID = after.clusterUUID; @@ -1091,12 +1105,15 @@ private static class MetadataDiff implements Diff { indices = DiffableUtils.diff(before.indices, after.indices, DiffableUtils.getStringKeySerializer()); templates = DiffableUtils.diff(before.templates, after.templates, DiffableUtils.getStringKeySerializer()); customs = DiffableUtils.diff(before.customs, after.customs, DiffableUtils.getStringKeySerializer(), CUSTOM_VALUE_SERIALIZER); + operatorState = DiffableUtils.diff(before.operatorState, after.operatorState, DiffableUtils.getStringKeySerializer()); } private static final DiffableUtils.DiffableValueReader INDEX_METADATA_DIFF_VALUE_READER = new DiffableUtils.DiffableValueReader<>(IndexMetadata::readFrom, IndexMetadata::readDiffFrom); private static final DiffableUtils.DiffableValueReader TEMPLATES_DIFF_VALUE_READER = new DiffableUtils.DiffableValueReader<>(IndexTemplateMetadata::readFrom, IndexTemplateMetadata::readDiffFrom); + private static final DiffableUtils.DiffableValueReader OPERATOR_DIFF_VALUE_READER = + new DiffableUtils.DiffableValueReader<>(OperatorMetadata::readFrom, OperatorMetadata::readDiffFrom); MetadataDiff(StreamInput in) throws IOException { clusterUUID = in.readString(); @@ -1113,6 +1130,7 @@ private static class MetadataDiff implements Diff { indices = DiffableUtils.readImmutableOpenMapDiff(in, DiffableUtils.getStringKeySerializer(), INDEX_METADATA_DIFF_VALUE_READER); templates = DiffableUtils.readImmutableOpenMapDiff(in, DiffableUtils.getStringKeySerializer(), TEMPLATES_DIFF_VALUE_READER); customs = DiffableUtils.readImmutableOpenMapDiff(in, DiffableUtils.getStringKeySerializer(), CUSTOM_VALUE_SERIALIZER); + operatorState = DiffableUtils.readImmutableOpenMapDiff(in, DiffableUtils.getStringKeySerializer(), OPERATOR_DIFF_VALUE_READER); } @Override @@ -1129,6 +1147,7 @@ public void writeTo(StreamOutput out) throws IOException { indices.writeTo(out); templates.writeTo(out); customs.writeTo(out); + operatorState.writeTo(out); } @Override @@ -1146,6 +1165,7 @@ public Metadata apply(Metadata part) { builder.indices(indices.apply(part.indices)); builder.templates(templates.apply(part.templates)); builder.customs(customs.apply(part.customs)); + builder.operatorState(operatorState.apply(part.operatorState)); return builder.build(); } } @@ -1187,6 +1207,10 @@ public static Metadata readFrom(StreamInput in) throws IOException { Custom customIndexMetadata = in.readNamedWriteable(Custom.class); builder.putCustom(customIndexMetadata.getWriteableName(), customIndexMetadata); } + size = in.readVInt(); + for (int i = 0; i < size; i++) { + builder.putOperatorState(OperatorMetadata.readFrom(in)); + } return builder.build(); } @@ -1214,6 +1238,7 @@ public void writeTo(StreamOutput out) throws IOException { } out.writeCollection(templates.values()); VersionedNamedWriteable.writeVersionedWritables(out, customs); + out.writeCollection(operatorState.values()); } public static Builder builder() { @@ -1248,6 +1273,8 @@ public static class Builder { private SortedMap previousIndicesLookup; + private final ImmutableOpenMap.Builder operatorState; + // If this is set to false we can skip checking #mappingsByHash for unused entries in #build(). Used as an optimization to save // the rather expensive call to #purgeUnusedEntries when building from another instance and we know that no mappings can have // become unused because no indices were updated or removed from this builder in a way that would cause unused entries in @@ -1275,6 +1302,7 @@ public Builder() { this.previousIndicesLookup = metadata.indicesLookup; this.mappingsByHash = new HashMap<>(metadata.mappingsByHash); this.checkForUnusedMappings = false; + this.operatorState = ImmutableOpenMap.builder(metadata.operatorState); } private Builder(Map mappingsByHash) { @@ -1283,6 +1311,7 @@ private Builder(Map mappingsByHash) { aliasedIndices = ImmutableOpenMap.builder(); templates = ImmutableOpenMap.builder(); customs = ImmutableOpenMap.builder(); + operatorState = ImmutableOpenMap.builder(); indexGraveyard(IndexGraveyard.builder().build()); // create new empty index graveyard to initialize previousIndicesLookup = null; this.mappingsByHash = new HashMap<>(mappingsByHash); @@ -1635,6 +1664,20 @@ public Builder customs(Map customs) { return this; } + public OperatorMetadata operatorState(String namespace) { + return operatorState.get(namespace); + } + + public Builder operatorState(Map operatorState) { + this.operatorState.putAllFromMap(operatorState); + return this; + } + + public Builder putOperatorState(OperatorMetadata metadata) { + operatorState.put(metadata.namespace(), metadata); + return this; + } + public Builder indexGraveyard(final IndexGraveyard indexGraveyard) { putCustom(IndexGraveyard.TYPE, indexGraveyard); return this; @@ -1839,7 +1882,8 @@ public Metadata build() { visibleClosedIndicesArray, indicesLookup, Collections.unmodifiableMap(mappingsByHash), - Version.fromId(oldestIndexVersionId) + Version.fromId(oldestIndexVersionId), + operatorState.build() ); } @@ -2159,6 +2203,12 @@ public static void toXContent(Metadata metadata, XContentBuilder builder, ToXCon } } + builder.startObject("operator"); + for (OperatorMetadata operatorMetadata : metadata.operatorState().values()) { + OperatorMetadata.Builder.toXContent(operatorMetadata, builder, params); + } + builder.endObject(); + builder.endObject(); } @@ -2202,6 +2252,10 @@ public static Metadata fromXContent(XContentParser parser) throws IOException { while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { builder.put(IndexTemplateMetadata.Builder.fromXContent(parser, parser.currentName())); } + } else if ("operator".equals(currentFieldName)) { + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + builder.putOperatorState(OperatorMetadata.Builder.fromXContent(parser, parser.currentName())); + } } else { try { Custom custom = parser.namedObject(Custom.class, currentFieldName, null); diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/OperatorHandlerMetadata.java b/server/src/main/java/org/elasticsearch/cluster/metadata/OperatorHandlerMetadata.java new file mode 100644 index 0000000000000..a933fef264633 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/OperatorHandlerMetadata.java @@ -0,0 +1,129 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.cluster.metadata; + +import org.elasticsearch.cluster.Diff; +import org.elasticsearch.cluster.SimpleDiffable; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.xcontent.ToXContent; +import org.elasticsearch.xcontent.ToXContentFragment; +import org.elasticsearch.xcontent.XContentBuilder; +import org.elasticsearch.xcontent.XContentParser; + +import java.io.IOException; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +/** + * Metadata class to hold the operator set keys for each operator handler + * + */ +public class OperatorHandlerMetadata implements SimpleDiffable, ToXContentFragment { + private final String name; + private final Set keys; + + public OperatorHandlerMetadata(String name, Set keys) { + this.name = name; + this.keys = keys; + } + + public String name() { + return this.name; + } + + public Set keys() { + return this.keys; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(name); + out.writeCollection(keys, StreamOutput::writeString); + } + + public static OperatorHandlerMetadata readFrom(StreamInput in) throws IOException { + Builder builder = new Builder(in.readString()).keys(in.readSet(StreamInput::readString)); + return builder.build(); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + Builder.toXContent(this, builder, params); + return builder; + } + + public static Diff readDiffFrom(StreamInput in) throws IOException { + return SimpleDiffable.readDiffFrom(OperatorHandlerMetadata::readFrom, in); + } + + public static class Builder { + private static final String HANDLER_KEYS = "keys"; + + private final String name; + private Set keys; + + public Builder(String name) { + this.name = name; + this.keys = new HashSet<>(); + } + + public Builder keys(Set keys) { + this.keys = keys; + return this; + } + + public OperatorHandlerMetadata build() { + return new OperatorHandlerMetadata(name, Collections.unmodifiableSet(keys)); + } + + /** + * Serializes the metadata to xContent + * + * @param metadata + * @param builder + * @param params + */ + public static void toXContent(OperatorHandlerMetadata metadata, XContentBuilder builder, ToXContent.Params params) + throws IOException { + builder.startObject(metadata.name()); + builder.stringListField(HANDLER_KEYS, metadata.keys); + builder.endObject(); + } + + /** + * Reads the metadata from xContent + * + * @param parser + * @return + * @throws IOException + */ + public static OperatorHandlerMetadata fromXContent(XContentParser parser) throws IOException { + Builder builder = new Builder(parser.currentName()); + + String currentFieldName = parser.currentName(); + XContentParser.Token token; + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + currentFieldName = parser.currentName(); + } else if (token == XContentParser.Token.START_ARRAY) { + if (HANDLER_KEYS.equals(currentFieldName)) { + Set handlerKeys = new HashSet<>(); + while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) { + handlerKeys.add(parser.text()); + } + builder.keys(handlerKeys); + } + } + } + return builder.build(); + } + } +} diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/OperatorMetadata.java b/server/src/main/java/org/elasticsearch/cluster/metadata/OperatorMetadata.java new file mode 100644 index 0000000000000..3e0b46629890d --- /dev/null +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/OperatorMetadata.java @@ -0,0 +1,155 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.cluster.metadata; + +import org.elasticsearch.ElasticsearchParseException; +import org.elasticsearch.cluster.Diff; +import org.elasticsearch.cluster.SimpleDiffable; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.xcontent.ToXContent; +import org.elasticsearch.xcontent.XContentBuilder; +import org.elasticsearch.xcontent.XContentParser; + +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +public class OperatorMetadata implements SimpleDiffable { + private final String namespace; + private final Long version; + private final Map handlers; + + public OperatorMetadata(String namespace, Long version, Map handlers) { + this.namespace = namespace; + this.version = version; + this.handlers = handlers; + } + + public String namespace() { + return namespace; + } + + public Long version() { + return version; + } + + public Map handlers() { + return handlers; + } + + public static OperatorMetadata readFrom(StreamInput in) throws IOException { + Builder builder = new Builder(in.readString()).version(in.readLong()); + + int handlersSize = in.readVInt(); + for (int i = 0; i < handlersSize; i++) { + OperatorHandlerMetadata handler = OperatorHandlerMetadata.readFrom(in); + builder.putHandler(handler); + } + + return builder.build(); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(namespace); + out.writeLong(version); + out.writeCollection(handlers.values()); + } + + public static Diff readDiffFrom(StreamInput in) throws IOException { + return SimpleDiffable.readDiffFrom(OperatorMetadata::readFrom, in); + } + + public static class Builder { + private static final String VERSION = "version"; + private static final String HANDLERS = "handlers"; + + private final String namespace; + private Long version; + private Map handlers; + + public Builder(String namespace) { + this.namespace = namespace; + this.version = 0L; + this.handlers = new HashMap<>(); + } + + public Builder version(Long version) { + this.version = version; + return this; + } + + public Builder handlerKeys(Map handlers) { + this.handlers = handlers; + return this; + } + + public Builder putHandler(OperatorHandlerMetadata handler) { + this.handlers.put(handler.name(), handler); + return this; + } + + public OperatorMetadata build() { + return new OperatorMetadata(namespace, version, Collections.unmodifiableMap(handlers)); + } + + /** + * Serializes the metadata to xContent + * + * @param operatorMetadata + * @param builder + * @param params + */ + public static void toXContent(OperatorMetadata operatorMetadata, XContentBuilder builder, ToXContent.Params params) + throws IOException { + builder.startObject(operatorMetadata.namespace()); + builder.field(VERSION, operatorMetadata.version()); + builder.startObject(HANDLERS); + for (OperatorHandlerMetadata handlerMetadata : operatorMetadata.handlers().values()) { + OperatorHandlerMetadata.Builder.toXContent(handlerMetadata, builder, params); + } + builder.endObject(); + } + + /** + * Reads the metadata from xContent + * + * @param parser + * @param namespace + * @return + * @throws IOException + */ + public static OperatorMetadata fromXContent(XContentParser parser, String namespace) throws IOException { + OperatorMetadata.Builder builder = new OperatorMetadata.Builder(namespace); + + String currentFieldName = parser.currentName(); + XContentParser.Token token; + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + currentFieldName = parser.currentName(); + } else if (token == XContentParser.Token.START_OBJECT) { + if (HANDLERS.equals(currentFieldName)) { + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + builder.putHandler(OperatorHandlerMetadata.Builder.fromXContent(parser)); + } + } else { + throw new ElasticsearchParseException("unknown key [{}] for index template", currentFieldName); + } + } else if (token.isValue()) { + if (VERSION.equals(currentFieldName)) { + builder.version(parser.longValue()); + } + } + } + return builder.build(); + } + } +} diff --git a/server/src/main/java/org/elasticsearch/operator/action/OperatorClusterUpdateSettingsAction.java b/server/src/main/java/org/elasticsearch/operator/action/OperatorClusterUpdateSettingsAction.java index 21fb31e6ff09b..8f517d9224317 100644 --- a/server/src/main/java/org/elasticsearch/operator/action/OperatorClusterUpdateSettingsAction.java +++ b/server/src/main/java/org/elasticsearch/operator/action/OperatorClusterUpdateSettingsAction.java @@ -22,14 +22,12 @@ import java.util.Set; import java.util.stream.Collectors; -import static org.elasticsearch.rest.action.admin.cluster.RestClusterUpdateSettingsAction.PERSISTENT; - /** * TODO: Add docs */ public class OperatorClusterUpdateSettingsAction implements OperatorHandler { - public static final String KEY = "cluster"; + public static final String KEY = "cluster_settings"; private final ClusterSettings clusterSettings; @@ -50,12 +48,10 @@ private ClusterUpdateSettingsRequest prepare(Object input, Set previousl Map persistentSettings = new HashMap<>(); Set toDelete = new HashSet<>(previouslySet); - if (source.containsKey(PERSISTENT)) { - ((Map) source.get(PERSISTENT)).forEach((k, v) -> { - persistentSettings.put(k, v); - toDelete.remove(k); - }); - } + source.forEach((k, v) -> { + persistentSettings.put(k, v); + toDelete.remove(k); + }); toDelete.forEach(k -> persistentSettings.put(k, null)); diff --git a/server/src/main/java/org/elasticsearch/operator/service/OperatorClusterStateController.java b/server/src/main/java/org/elasticsearch/operator/service/OperatorClusterStateController.java index 385de7ffcf56b..11dd695db67ad 100644 --- a/server/src/main/java/org/elasticsearch/operator/service/OperatorClusterStateController.java +++ b/server/src/main/java/org/elasticsearch/operator/service/OperatorClusterStateController.java @@ -8,14 +8,22 @@ package org.elasticsearch.operator.service; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.Version; import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.metadata.Metadata; +import org.elasticsearch.cluster.metadata.OperatorHandlerMetadata; +import org.elasticsearch.cluster.metadata.OperatorMetadata; import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.operator.OperatorHandler; import org.elasticsearch.operator.TransformState; +import org.elasticsearch.xcontent.ConstructingObjectParser; +import org.elasticsearch.xcontent.ParseField; import org.elasticsearch.xcontent.XContentParser; import java.io.IOException; -import java.util.HashSet; +import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -24,9 +32,16 @@ import java.util.stream.Collectors; /** - * TODO: Write docs + * Controller class for applying file based settings to ClusterState. + * This class contains the logic about validation, ordering and applying of + * the cluster state specified in a file. */ public class OperatorClusterStateController { + private static final Logger logger = LogManager.getLogger(FileSettingsService.class); + + public static final String SETTINGS = "settings"; + public static final String METADATA = "metadata"; + Map> handlers = null; final ClusterService clusterService; @@ -38,32 +53,84 @@ public void initHandlers(List> handlerList) { handlers = handlerList.stream().collect(Collectors.toMap(OperatorHandler::key, Function.identity())); } + static class SettingsFile { + public static final ParseField STATE_FIELD = new ParseField("state"); + public static final ParseField METADATA_FIELD = new ParseField("metadata"); + @SuppressWarnings("unchecked") + private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( + "operator_state", + a -> new SettingsFile((Map) a[0], (OperatorStateVersionMetadata) a[1]) + ); + static { + PARSER.declareObject(ConstructingObjectParser.constructorArg(), (p, c) -> p.map(), STATE_FIELD); + PARSER.declareObject(ConstructingObjectParser.constructorArg(), OperatorStateVersionMetadata::parse, METADATA_FIELD); + } + + Map state; + OperatorStateVersionMetadata metadata; + + SettingsFile(Map state, OperatorStateVersionMetadata metadata) { + this.state = state; + this.metadata = metadata; + } + } + public ClusterState process(String namespace, XContentParser parser) throws IOException { - Map source = parser.map(); + SettingsFile operatorStateFileContent = SettingsFile.PARSER.apply(parser, null); + + Map operatorState = operatorStateFileContent.state; + OperatorStateVersionMetadata stateVersionMetadata = operatorStateFileContent.metadata; - LinkedHashSet orderedHandlers = orderedStateHandlers(source.keySet()); + LinkedHashSet orderedHandlers = orderedStateHandlers(operatorState.keySet()); ClusterState state = clusterService.state(); - // TODO: extract the namespace keys from the state, if any and pass them to each transform + OperatorMetadata existingMetadata = state.metadata().operatorState(namespace); + + checkMetadataVersion(existingMetadata, stateVersionMetadata); + + OperatorMetadata.Builder operatorMetadataBuilder = new OperatorMetadata.Builder(namespace).version(stateVersionMetadata.version()); for (var handlerKey : orderedHandlers) { OperatorHandler handler = handlers.get(handlerKey); try { - // TODO: fetch and pass previous keys for handler to be able to delete - state = handler.transform(source.get(handlerKey), new TransformState(state, new HashSet<>())).state(); + Set existingKeys = keysForHandler(existingMetadata, handlerKey); + TransformState transformState = handler.transform(operatorState.get(handlerKey), new TransformState(state, existingKeys)); + state = transformState.state(); + operatorMetadataBuilder.putHandler(new OperatorHandlerMetadata.Builder(handlerKey).keys(transformState.keys()).build()); } catch (Exception e) { throw new IllegalStateException("Error processing state change request for: " + handler.key(), e); } } - // TODO: extract the keys written for this namespace, and store them in the cluster state + ClusterState.Builder stateBuilder = new ClusterState.Builder(state); + Metadata.Builder metadataBuilder = Metadata.builder(state.metadata()).putOperatorState(operatorMetadataBuilder.build()); + state = stateBuilder.metadata(metadataBuilder).build(); + // TODO: call a clusterService state update task // TODO: call reroute service return state; } + private Set keysForHandler(OperatorMetadata operatorMetadata, String handlerKey) { + if (operatorMetadata == null || operatorMetadata.handlers().get(handlerKey) == null) { + return Collections.emptySet(); + } + + return operatorMetadata.handlers().get(handlerKey).keys(); + } + + void checkMetadataVersion(OperatorMetadata existingMetadata, OperatorStateVersionMetadata stateVersionMetadata) { + if (Version.CURRENT.before(stateVersionMetadata.minCompatibleVersion())) { + throw new IllegalStateException("Newer version operator cluster state"); + } + + if (existingMetadata != null && existingMetadata.version() >= stateVersionMetadata.version()) { + throw new IllegalStateException("Cluster state not updated because version is less or equal to before"); + } + } + LinkedHashSet orderedStateHandlers(Set keys) { LinkedHashSet orderedHandlers = new LinkedHashSet<>(); LinkedHashSet dependencyStack = new LinkedHashSet<>(); diff --git a/server/src/main/java/org/elasticsearch/operator/service/OperatorStateVersionMetadata.java b/server/src/main/java/org/elasticsearch/operator/service/OperatorStateVersionMetadata.java new file mode 100644 index 0000000000000..c723e00a5f3a9 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/operator/service/OperatorStateVersionMetadata.java @@ -0,0 +1,57 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.operator.service; + +import org.elasticsearch.Version; +import org.elasticsearch.xcontent.ConstructingObjectParser; +import org.elasticsearch.xcontent.ParseField; +import org.elasticsearch.xcontent.XContentParser; + +/** + * File settings metadata class that holds information about + * versioning and Elasticsearch version compatibility + */ +public class OperatorStateVersionMetadata { + public static final ParseField VERSION = new ParseField("version"); + public static final ParseField COMPATIBILITY = new ParseField("compatibility"); + + private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( + "operator_settings_metadata", + a -> { + Long updateId = Long.parseLong((String) a[0]); + Version minCompatVersion = Version.fromString((String) a[1]); + + return new OperatorStateVersionMetadata(updateId, minCompatVersion); + } + ); + static { + PARSER.declareString(ConstructingObjectParser.constructorArg(), VERSION); + PARSER.declareString(ConstructingObjectParser.constructorArg(), COMPATIBILITY); + } + + private Long version; + private Version compatibleWith; + + public OperatorStateVersionMetadata(Long version, Version compatibleWith) { + this.version = version; + this.compatibleWith = compatibleWith; + } + + public static OperatorStateVersionMetadata parse(XContentParser parser, Void v) { + return PARSER.apply(parser, v); + } + + public Long version() { + return version; + } + + public Version minCompatibleVersion() { + return compatibleWith; + } +} diff --git a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/action/operator/OperatorILMControllerTests.java b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/action/operator/OperatorILMControllerTests.java index f4c5ee5d4e210..2d83f828ed051 100644 --- a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/action/operator/OperatorILMControllerTests.java +++ b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/action/operator/OperatorILMControllerTests.java @@ -15,8 +15,8 @@ import org.elasticsearch.common.settings.ClusterSettings; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.license.XPackLicenseState; -import org.elasticsearch.operator.service.OperatorClusterStateController; import org.elasticsearch.operator.action.OperatorClusterUpdateSettingsAction; +import org.elasticsearch.operator.service.OperatorClusterStateController; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.xcontent.NamedXContentRegistry; import org.elasticsearch.xcontent.ParseField; @@ -98,34 +98,34 @@ public void testOperatorController() throws IOException { String testJSON = """ { - "cluster": { - "persistent": { - "indices.recovery.max_bytes_per_sec": "50mb" - }, - "transient": { - "cluster.routing.allocation.enable": "none" - } - }, - "ilm": { - "my_timeseries_lifecycle": { - "policy": { - "phases": { - "warm": { - "min_age": "10s", - "actions": { - } - }, - "delete": { - "min_age": "30s", - "actions": { - } - } - } - } - } - } - } - """; + "metadata": { + "version": "1234", + "compatibility": "8.4.0" + }, + "state": { + "cluster_settings": { + "indices.recovery.max_bytes_per_sec": "50mb" + }, + "ilm": { + "my_timeseries_lifecycle": { + "policy": { + "phases": { + "warm": { + "min_age": "10s", + "actions": { + } + }, + "delete": { + "min_age": "30s", + "actions": { + } + } + } + } + } + } + } + }"""; try (XContentParser parser = XContentType.JSON.xContent().createParser(XContentParserConfiguration.EMPTY, testJSON)) { assertEquals( From 5618019fc6808d39bb31bc1763510e0dd80e89a5 Mon Sep 17 00:00:00 2001 From: Nikola Grcevski Date: Tue, 31 May 2022 15:55:02 -0400 Subject: [PATCH 15/48] Make file service work --- .../elasticsearch/action/ActionModule.java | 4 + .../cluster/metadata/OperatorMetadata.java | 1 + .../common/settings/ClusterSettings.java | 4 +- .../java/org/elasticsearch/node/Node.java | 5 +- .../operator/service/FileSettingsService.java | 117 +++++++++++------- .../OperatorClusterStateController.java | 22 +++- .../service/OperatorUpdateStateTask.java | 68 ++++++++++ 7 files changed, 175 insertions(+), 46 deletions(-) create mode 100644 server/src/main/java/org/elasticsearch/operator/service/OperatorUpdateStateTask.java diff --git a/server/src/main/java/org/elasticsearch/action/ActionModule.java b/server/src/main/java/org/elasticsearch/action/ActionModule.java index 93231676f0226..c55bd150f8ca9 100644 --- a/server/src/main/java/org/elasticsearch/action/ActionModule.java +++ b/server/src/main/java/org/elasticsearch/action/ActionModule.java @@ -934,4 +934,8 @@ public ActionFilters getActionFilters() { public RestController getRestController() { return restController; } + + public OperatorClusterStateController getOperatorController() { + return operatorController; + } } diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/OperatorMetadata.java b/server/src/main/java/org/elasticsearch/cluster/metadata/OperatorMetadata.java index 3e0b46629890d..6c2912ac8b87d 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/OperatorMetadata.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/OperatorMetadata.java @@ -117,6 +117,7 @@ public static void toXContent(OperatorMetadata operatorMetadata, XContentBuilder OperatorHandlerMetadata.Builder.toXContent(handlerMetadata, builder, params); } builder.endObject(); + builder.endObject(); } /** diff --git a/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java b/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java index 0ed272d454729..fe61a252d912f 100644 --- a/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java +++ b/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java @@ -88,6 +88,7 @@ import org.elasticsearch.monitor.process.ProcessService; import org.elasticsearch.node.Node; import org.elasticsearch.node.NodeRoleSettings; +import org.elasticsearch.operator.service.FileSettingsService; import org.elasticsearch.persistent.PersistentTasksClusterService; import org.elasticsearch.persistent.decider.EnableAssignmentDecider; import org.elasticsearch.plugins.PluginsService; @@ -511,7 +512,8 @@ public void apply(Settings value, Settings current, Settings previous) { IndexingPressure.MAX_INDEXING_BYTES, ShardLimitValidator.SETTING_CLUSTER_MAX_SHARDS_PER_NODE_FROZEN, DataTier.ENFORCE_DEFAULT_TIER_PREFERENCE_SETTING, - ReadinessService.PORT + ReadinessService.PORT, + FileSettingsService.OPERATOR_DIR_NAME ); static List> BUILT_IN_SETTING_UPGRADERS = Collections.emptyList(); diff --git a/server/src/main/java/org/elasticsearch/node/Node.java b/server/src/main/java/org/elasticsearch/node/Node.java index 4bbadc1b5a566..134e54ce4497a 100644 --- a/server/src/main/java/org/elasticsearch/node/Node.java +++ b/server/src/main/java/org/elasticsearch/node/Node.java @@ -977,7 +977,10 @@ protected Node( modules.add(b -> b.bind(ReadinessService.class).toInstance(new ReadinessService(clusterService, environment))); } - modules.add(b -> b.bind(FileSettingsService.class).toInstance(new FileSettingsService(clusterService, environment))); + modules.add( + b -> b.bind(FileSettingsService.class) + .toInstance(new FileSettingsService(clusterService, actionModule.getOperatorController(), environment)) + ); injector = modules.createInjector(); diff --git a/server/src/main/java/org/elasticsearch/operator/service/FileSettingsService.java b/server/src/main/java/org/elasticsearch/operator/service/FileSettingsService.java index 3c7a3c89781ff..fd35ba343f6c0 100644 --- a/server/src/main/java/org/elasticsearch/operator/service/FileSettingsService.java +++ b/server/src/main/java/org/elasticsearch/operator/service/FileSettingsService.java @@ -18,6 +18,9 @@ import org.elasticsearch.common.settings.Setting; import org.elasticsearch.core.PathUtils; import org.elasticsearch.env.Environment; +import org.elasticsearch.xcontent.XContentParser; +import org.elasticsearch.xcontent.XContentParserConfiguration; +import org.elasticsearch.xcontent.XContentType; import java.io.IOException; import java.nio.file.ClosedWatchServiceException; @@ -33,6 +36,7 @@ public class FileSettingsService extends AbstractLifecycleComponent implements C private static final Logger logger = LogManager.getLogger(FileSettingsService.class); private final ClusterService clusterService; + private final OperatorClusterStateController controller; private final Environment environment; private WatchService watchService; // null; @@ -42,22 +46,28 @@ public class FileSettingsService extends AbstractLifecycleComponent implements C private volatile boolean active = false; - public static final Setting OPERATOR_SETTINGS = Setting.simpleString( - "readiness.port", - "operatorSettings.json", + public static final Setting OPERATOR_DIR_NAME = Setting.simpleString( + "path.config.operator_dir_name", + "operator", Setting.Property.NodeScope ); - public FileSettingsService(ClusterService clusterService, Environment environment) { + public FileSettingsService(ClusterService clusterService, OperatorClusterStateController controller, Environment environment) { this.clusterService = clusterService; + this.controller = controller; this.environment = environment; clusterService.addListener(this); } + // package private for testing + Path operatorSettingsDir() { + String dirPath = OPERATOR_DIR_NAME.get(environment.settings()); + return environment.configFile().toAbsolutePath().resolve(dirPath); + } + // package private for testing Path operatorSettingsFile() { - String fileName = OPERATOR_SETTINGS.get(environment.settings()); - return environment.configFile().resolve(fileName); + return operatorSettingsDir().resolve("settings.json"); } // package private for testing @@ -88,10 +98,14 @@ protected void doStop() { @Override protected void doClose() {} + private boolean currentNodeMaster(ClusterState clusterState) { + return clusterState.nodes().getMasterNodeId().equals(clusterState.nodes().getLocalNodeId()); + } + @Override public void clusterChanged(ClusterChangedEvent event) { ClusterState clusterState = event.state(); - setWatching(clusterState.nodes().getMasterNodeId().equals(clusterState.nodes().getLocalNodeId())); + setWatching(currentNodeMaster(clusterState)); } private void setWatching(boolean watching) { @@ -115,31 +129,26 @@ synchronized void startWatcher() { logger.info("starting file settings watcher ..."); - Path path = operatorSettingsFile(); - Path configDir = path.getParent(); - - if (Files.exists(configDir) == false) { - logger.warn("file based settings service disabled because config dir [{}] doesn't exist", configDir); - return; - } - - try { - this.lastUpdatedTime = watchedFileTimestamp(path); - if (lastUpdatedTime > 0L) { - processFileSettings(path); - } - } catch (IOException e) { - logger.warn("encountered I/O exception trying to read file attributes for the file based settings", e); - } + Path settingsDir = operatorSettingsDir(); try { this.watchService = PathUtils.getDefaultFileSystem().newWatchService(); - configDir.register( - watchService, - StandardWatchEventKinds.ENTRY_MODIFY, - StandardWatchEventKinds.ENTRY_CREATE, - StandardWatchEventKinds.ENTRY_DELETE - ); + if (Files.exists(settingsDir)) { + Path settingsFilePath = operatorSettingsFile(); + try { + this.lastUpdatedTime = watchedFileTimestamp(settingsFilePath); + if (lastUpdatedTime > 0L) { + logger.info("found initial operator settings file [{}], applying...", settingsFilePath); + processFileSettings(settingsFilePath); + } + } catch (IOException e) { + logger.warn("encountered I/O exception trying to read file attributes for the file based settings", e); + } + enableSettingsWatcher(settingsDir); + } else { + logger.info("operator settings directory [{}] not found, will watch for its creation...", settingsDir); + enableSettingsWatcher(environment.configFile()); + } } catch (Exception e) { if (watchService != null) { try { @@ -162,19 +171,28 @@ synchronized void startWatcher() { while ((key = watchService.take()) != null) { // Reading and interpreting watch service events can vary from platform to platform. // After we get an indication that something has changed, we check the timestamp of our desired file. - try { - long updatedTime = watchedFileTimestamp(path); - if (updatedTime > lastUpdatedTime) { - this.lastUpdatedTime = updatedTime; - processFileSettings(path); + if (Files.exists(settingsDir)) { + try { + key.cancel(); + enableSettingsWatcher(settingsDir); + + Path path = operatorSettingsFile(); + + long updatedTime = watchedFileTimestamp(path); + if (updatedTime > lastUpdatedTime) { + this.lastUpdatedTime = updatedTime; + processFileSettings(path); + } + } catch (IOException e) { + logger.warn("unable to watch or read operator settings file", e); } - } catch (IOException e) { - logger.warn("unable to read file attributes of " + path, e); + } else { + key.cancel(); + enableSettingsWatcher(environment.configFile()); } - key.reset(); } - } catch (InterruptedException | ClosedWatchServiceException e) { - logger.debug("encountered exception watching. Shutting down watcher thread.", e); + } catch (InterruptedException | ClosedWatchServiceException | IOException e) { + logger.debug("encountered exception watching, shutting down watcher thread.", e); } finally { watcherThreadLatch.countDown(); } @@ -188,7 +206,7 @@ synchronized void stopWatcher() { watchService.close(); watcherThreadLatch.await(); } catch (IOException | InterruptedException e) { - logger.info("Encountered exception while closing watch service", e); + logger.info("encountered exception while closing watch service", e); } finally { watchService = null; logger.info("watcher service stopped"); @@ -198,8 +216,23 @@ synchronized void stopWatcher() { } } + private void enableSettingsWatcher(Path settingsDir) throws IOException { + settingsDir.register( + watchService, + StandardWatchEventKinds.ENTRY_MODIFY, + StandardWatchEventKinds.ENTRY_CREATE, + StandardWatchEventKinds.ENTRY_DELETE + ); + } + void processFileSettings(Path path) { - // TODO: implement me - logger.info("settings file changed event"); + logger.info("Processing path [{}]", path); + try ( + XContentParser parser = XContentType.JSON.xContent().createParser(XContentParserConfiguration.EMPTY, Files.newInputStream(path)) + ) { + controller.process("operator", parser); + } catch (Exception e) { + logger.error("Error parsing operator settings json file", e); + } } } diff --git a/server/src/main/java/org/elasticsearch/operator/service/OperatorClusterStateController.java b/server/src/main/java/org/elasticsearch/operator/service/OperatorClusterStateController.java index 11dd695db67ad..80d38cb597a16 100644 --- a/server/src/main/java/org/elasticsearch/operator/service/OperatorClusterStateController.java +++ b/server/src/main/java/org/elasticsearch/operator/service/OperatorClusterStateController.java @@ -11,11 +11,15 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.elasticsearch.Version; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.ActionResponse; import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.ClusterStateTaskConfig; import org.elasticsearch.cluster.metadata.Metadata; import org.elasticsearch.cluster.metadata.OperatorHandlerMetadata; import org.elasticsearch.cluster.metadata.OperatorMetadata; import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.common.Priority; import org.elasticsearch.operator.OperatorHandler; import org.elasticsearch.operator.TransformState; import org.elasticsearch.xcontent.ConstructingObjectParser; @@ -99,6 +103,7 @@ public ClusterState process(String namespace, XContentParser parser) throws IOEx state = transformState.state(); operatorMetadataBuilder.putHandler(new OperatorHandlerMetadata.Builder(handlerKey).keys(transformState.keys()).build()); } catch (Exception e) { + // TODO: Collect all errors, store them in the cluster state metadata, throw at the end with all of them throw new IllegalStateException("Error processing state change request for: " + handler.key(), e); } } @@ -107,8 +112,21 @@ public ClusterState process(String namespace, XContentParser parser) throws IOEx Metadata.Builder metadataBuilder = Metadata.builder(state.metadata()).putOperatorState(operatorMetadataBuilder.build()); state = stateBuilder.metadata(metadataBuilder).build(); - // TODO: call a clusterService state update task - // TODO: call reroute service + // TODO: Retry, maybe use RetryableAction? + clusterService.submitStateUpdateTask("operator state [ " + namespace + "]", new OperatorUpdateStateTask(new ActionListener<>() { + @Override + public void onResponse(ActionResponse.Empty empty) { + logger.info("Successfully applied new cluster state for namespace [{}]", namespace); + } + + @Override + public void onFailure(Exception e) { + logger.error("Failed to apply operator cluster state", e); + } + }), + ClusterStateTaskConfig.build(Priority.URGENT), + new OperatorUpdateStateTask.OperatorUpdateStateTaskExecutor(namespace, state, clusterService.getRerouteService()) + ); return state; } diff --git a/server/src/main/java/org/elasticsearch/operator/service/OperatorUpdateStateTask.java b/server/src/main/java/org/elasticsearch/operator/service/OperatorUpdateStateTask.java new file mode 100644 index 0000000000000..07ce16e40aa84 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/operator/service/OperatorUpdateStateTask.java @@ -0,0 +1,68 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.operator.service; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.ActionResponse; +import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.ClusterStateTaskExecutor; +import org.elasticsearch.cluster.ClusterStateTaskListener; +import org.elasticsearch.cluster.routing.RerouteService; +import org.elasticsearch.common.Priority; + +import java.util.List; + +/** + * Generic operator cluster state update task + * + * @param listener + */ +public record OperatorUpdateStateTask(ActionListener listener) implements ClusterStateTaskListener { + private static final Logger logger = LogManager.getLogger(FileSettingsService.class); + + @Override + public void onFailure(Exception e) { + listener.onFailure(e); + } + + /** + * Operator update cluster state task executor + * + * @param namespace of the state we are updating + * @param rerouteService instance of RerouteService so we can execute reroute after cluster state is published + */ + public record OperatorUpdateStateTaskExecutor(String namespace, ClusterState newState, RerouteService rerouteService) + implements + ClusterStateTaskExecutor { + + @Override + public ClusterState execute(ClusterState currentState, List> taskContexts) throws Exception { + for (final var taskContext : taskContexts) { + taskContext.success( + taskContext.getTask().listener().delegateFailure((l, s) -> l.onResponse(ActionResponse.Empty.INSTANCE)) + ); + } + return newState; + } + + @Override + public void clusterStatePublished(ClusterState newClusterState) { + rerouteService.reroute( + "reroute after applying operator cluster state for namespace [" + namespace + "]", + Priority.NORMAL, + ActionListener.wrap( + r -> logger.trace("reroute after applying operator cluster state for [{}] succeeded", namespace), + e -> logger.debug("reroute after applying operator cluster state failed", e) + ) + ); + } + } +} From 5222fd1bd4941d4239e26d0cad63149e7f807e8c Mon Sep 17 00:00:00 2001 From: Nikola Grcevski Date: Tue, 31 May 2022 15:58:40 -0400 Subject: [PATCH 16/48] Update test --- .../OperatorClusterStateControllerTests.java | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/server/src/test/java/org/elasticsearch/operator/OperatorClusterStateControllerTests.java b/server/src/test/java/org/elasticsearch/operator/OperatorClusterStateControllerTests.java index 0fe82fdf4ebea..28fe050d3a16c 100644 --- a/server/src/test/java/org/elasticsearch/operator/OperatorClusterStateControllerTests.java +++ b/server/src/test/java/org/elasticsearch/operator/OperatorClusterStateControllerTests.java @@ -41,14 +41,15 @@ public void testOperatorController() throws IOException { String testJSON = """ { - "cluster": { - "persistent": { - "indices.recovery.max_bytes_per_sec": "50mb" - }, - "transient": { - "cluster.routing.allocation.enable": "none" - } - } + "metadata": { + "version": "1234", + "compatibility": "8.4.0" + }, + "state": { + "cluster_settings": { + "indices.recovery.max_bytes_per_sec": "50mb" + } + } } """; From 1505524a5b5d12e1f7a159caf6c8f8584481f4ff Mon Sep 17 00:00:00 2001 From: Nikola Grcevski Date: Thu, 2 Jun 2022 19:52:15 -0400 Subject: [PATCH 17/48] Add error detection and save --- .../metadata/OperatorErrorMetadata.java | 143 ++++++++++++++++++ .../cluster/metadata/OperatorMetadata.java | 46 +++++- .../operator/service/FileSettingsService.java | 75 +++++---- .../OperatorClusterStateController.java | 53 +++++-- .../service/OperatorUpdateErrorTask.java | 74 +++++++++ 5 files changed, 350 insertions(+), 41 deletions(-) create mode 100644 server/src/main/java/org/elasticsearch/cluster/metadata/OperatorErrorMetadata.java create mode 100644 server/src/main/java/org/elasticsearch/operator/service/OperatorUpdateErrorTask.java diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/OperatorErrorMetadata.java b/server/src/main/java/org/elasticsearch/cluster/metadata/OperatorErrorMetadata.java new file mode 100644 index 0000000000000..7b65c0c3cfc54 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/OperatorErrorMetadata.java @@ -0,0 +1,143 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.cluster.metadata; + +import org.elasticsearch.cluster.Diff; +import org.elasticsearch.cluster.SimpleDiffable; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.xcontent.ToXContentFragment; +import org.elasticsearch.xcontent.XContentBuilder; +import org.elasticsearch.xcontent.XContentParser; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Metadata class to hold the operator set keys for each operator handler + * + */ +public class OperatorErrorMetadata implements SimpleDiffable, ToXContentFragment { + private final Long version; + private final List errors; + + public OperatorErrorMetadata(Long version, List errors) { + this.version = version; + this.errors = errors; + } + + public Long version() { + return this.version; + } + + public List errors() { + return this.errors; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeLong(version); + out.writeCollection(errors, StreamOutput::writeString); + } + + public static OperatorErrorMetadata readFrom(StreamInput in) throws IOException { + Builder builder = new Builder().version(in.readLong()).errors(in.readList(StreamInput::readString)); + return builder.build(); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + Builder.toXContent(this, builder, params); + return builder; + } + + public static Diff readDiffFrom(StreamInput in) throws IOException { + return SimpleDiffable.readDiffFrom(OperatorErrorMetadata::readFrom, in); + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private static final String ERRORS = "errors"; + private static final String VERSION = "version"; + + private Long version; + private List errors; + + public Builder() { + this.version = 0L; + this.errors = new ArrayList<>(); + } + + public Builder version(Long version) { + this.version = version; + return this; + } + + public Builder errors(List errors) { + this.errors = errors; + return this; + } + + public OperatorErrorMetadata build() { + return new OperatorErrorMetadata(version, Collections.unmodifiableList(errors)); + } + + /** + * Serializes the metadata to xContent + * + * @param metadata + * @param builder + * @param params + */ + public static void toXContent(OperatorErrorMetadata metadata, XContentBuilder builder, Params params) + throws IOException { + builder.startObject(); + builder.field(VERSION, metadata.version); + builder.stringListField(ERRORS, metadata.errors); + builder.endObject(); + } + + /** + * Reads the metadata from xContent + * + * @param parser + * @return + * @throws IOException + */ + public static OperatorErrorMetadata fromXContent(XContentParser parser) throws IOException { + Builder builder = new Builder(); + + String currentFieldName = parser.currentName(); + XContentParser.Token token; + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + currentFieldName = parser.currentName(); + } else if (token == XContentParser.Token.START_ARRAY) { + if (ERRORS.equals(currentFieldName)) { + List errors = new ArrayList<>(); + while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) { + errors.add(parser.text()); + } + builder.errors(errors); + } + } else if (token.isValue()) { + if (VERSION.equals(currentFieldName)) { + builder.version(parser.longValue()); + } + } + } + return builder.build(); + } + } +} diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/OperatorMetadata.java b/server/src/main/java/org/elasticsearch/cluster/metadata/OperatorMetadata.java index 6c2912ac8b87d..75dd75d51b12b 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/OperatorMetadata.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/OperatorMetadata.java @@ -26,11 +26,17 @@ public class OperatorMetadata implements SimpleDiffable { private final String namespace; private final Long version; private final Map handlers; + private final OperatorErrorMetadata errorMetadata; - public OperatorMetadata(String namespace, Long version, Map handlers) { + public OperatorMetadata( + String namespace, + Long version, + Map handlers, + OperatorErrorMetadata errorMetadata) { this.namespace = namespace; this.version = version; this.handlers = handlers; + this.errorMetadata = errorMetadata; } public String namespace() { @@ -41,6 +47,10 @@ public Long version() { return version; } + public OperatorErrorMetadata errorMetadata() { + return errorMetadata; + } + public Map handlers() { return handlers; } @@ -54,6 +64,7 @@ public static OperatorMetadata readFrom(StreamInput in) throws IOException { builder.putHandler(handler); } + builder.errorMetadata(in.readOptionalWriteable(OperatorErrorMetadata::readFrom)); return builder.build(); } @@ -62,24 +73,45 @@ public void writeTo(StreamOutput out) throws IOException { out.writeString(namespace); out.writeLong(version); out.writeCollection(handlers.values()); + out.writeOptionalWriteable(errorMetadata); } public static Diff readDiffFrom(StreamInput in) throws IOException { return SimpleDiffable.readDiffFrom(OperatorMetadata::readFrom, in); } + public static Builder builder(String namespace) { + return new Builder(namespace); + } + + public static Builder builder(String namespace, OperatorMetadata metadata) { + return new Builder(namespace, metadata); + } + public static class Builder { private static final String VERSION = "version"; private static final String HANDLERS = "handlers"; + private static final String ERRORS_METADATA = "errors"; private final String namespace; private Long version; private Map handlers; + OperatorErrorMetadata errorMetadata; public Builder(String namespace) { this.namespace = namespace; this.version = 0L; this.handlers = new HashMap<>(); + this.errorMetadata = null; + } + + public Builder(String namespace, OperatorMetadata metadata) { + this(namespace); + if (metadata != null) { + this.version = metadata.version; + this.handlers = metadata.handlers; + this.errorMetadata = metadata.errorMetadata; + } } public Builder version(Long version) { @@ -87,6 +119,11 @@ public Builder version(Long version) { return this; } + public Builder errorMetadata(OperatorErrorMetadata errorMetadata) { + this.errorMetadata = errorMetadata; + return this; + } + public Builder handlerKeys(Map handlers) { this.handlers = handlers; return this; @@ -98,7 +135,7 @@ public Builder putHandler(OperatorHandlerMetadata handler) { } public OperatorMetadata build() { - return new OperatorMetadata(namespace, version, Collections.unmodifiableMap(handlers)); + return new OperatorMetadata(namespace, version, Collections.unmodifiableMap(handlers), errorMetadata); } /** @@ -117,6 +154,7 @@ public static void toXContent(OperatorMetadata operatorMetadata, XContentBuilder OperatorHandlerMetadata.Builder.toXContent(handlerMetadata, builder, params); } builder.endObject(); + builder.field(ERRORS_METADATA, operatorMetadata.errorMetadata); builder.endObject(); } @@ -141,8 +179,10 @@ public static OperatorMetadata fromXContent(XContentParser parser, String namesp while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { builder.putHandler(OperatorHandlerMetadata.Builder.fromXContent(parser)); } + } else if (ERRORS_METADATA.equals(currentFieldName)) { + builder.errorMetadata(OperatorErrorMetadata.Builder.fromXContent(parser)); } else { - throw new ElasticsearchParseException("unknown key [{}] for index template", currentFieldName); + throw new ElasticsearchParseException("unknown key [{}] for operator metadata", currentFieldName); } } else if (token.isValue()) { if (VERSION.equals(currentFieldName)) { diff --git a/server/src/main/java/org/elasticsearch/operator/service/FileSettingsService.java b/server/src/main/java/org/elasticsearch/operator/service/FileSettingsService.java index fd35ba343f6c0..b8b399fb4ac81 100644 --- a/server/src/main/java/org/elasticsearch/operator/service/FileSettingsService.java +++ b/server/src/main/java/org/elasticsearch/operator/service/FileSettingsService.java @@ -23,7 +23,6 @@ import org.elasticsearch.xcontent.XContentType; import java.io.IOException; -import java.nio.file.ClosedWatchServiceException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardWatchEventKinds; @@ -35,6 +34,9 @@ public class FileSettingsService extends AbstractLifecycleComponent implements ClusterStateListener { private static final Logger logger = LogManager.getLogger(FileSettingsService.class); + private static final String SETTINGS_FILE_NAME = "settings.json"; + private static final String NAMESPACE = "file_settings"; + private final ClusterService clusterService; private final OperatorClusterStateController controller; private final Environment environment; @@ -42,7 +44,7 @@ public class FileSettingsService extends AbstractLifecycleComponent implements C private WatchService watchService; // null; private CountDownLatch watcherThreadLatch; - private volatile long lastUpdatedTime = 0L; + private volatile FileUpdateState fileUpdateState = null; private volatile boolean active = false; @@ -67,17 +69,24 @@ Path operatorSettingsDir() { // package private for testing Path operatorSettingsFile() { - return operatorSettingsDir().resolve("settings.json"); + return operatorSettingsDir().resolve(SETTINGS_FILE_NAME); } - // package private for testing - static long watchedFileTimestamp(Path path) throws IOException { + boolean watchedFileChanged(Path path) throws IOException { if (Files.exists(path) == false) { - return 0; + return false; } + + FileUpdateState previousUpdateState = fileUpdateState; + BasicFileAttributes attr = Files.readAttributes(path, BasicFileAttributes.class); + fileUpdateState = new FileUpdateState( + attr.lastModifiedTime().toMillis(), + path.toRealPath().toString(), + attr.fileKey() + ); - return attr.lastModifiedTime().toMillis(); + return (previousUpdateState == null || previousUpdateState.equals(fileUpdateState) == false); } @Override @@ -135,14 +144,9 @@ synchronized void startWatcher() { this.watchService = PathUtils.getDefaultFileSystem().newWatchService(); if (Files.exists(settingsDir)) { Path settingsFilePath = operatorSettingsFile(); - try { - this.lastUpdatedTime = watchedFileTimestamp(settingsFilePath); - if (lastUpdatedTime > 0L) { - logger.info("found initial operator settings file [{}], applying...", settingsFilePath); - processFileSettings(settingsFilePath); - } - } catch (IOException e) { - logger.warn("encountered I/O exception trying to read file attributes for the file based settings", e); + if (Files.exists(settingsFilePath)) { + logger.info("found initial operator settings file [{}], applying...", settingsFilePath); + processFileSettings(settingsFilePath); } enableSettingsWatcher(settingsDir); } else { @@ -169,29 +173,40 @@ synchronized void startWatcher() { WatchKey key; while ((key = watchService.take()) != null) { - // Reading and interpreting watch service events can vary from platform to platform. - // After we get an indication that something has changed, we check the timestamp of our desired file. + // Reading and interpreting watch service events can vary from platform to platform. E.g: + // MacOS symlink delete and set (rm -rf operator && ln -s /file_settings/ operator): + // ENTRY_MODIFY:operator + // ENTRY_CREATE:settings.json + // ENTRY_MODIFY:settings.json + // Linux in Docker symlink delete and set (rm -rf operator && ln -s /file_settings/ operator): + // ENTRY_CREATE:operator + // After we get an indication that something has changed, we check the timestamp, file id, + // real path of our desired file. if (Files.exists(settingsDir)) { try { - key.cancel(); - enableSettingsWatcher(settingsDir); - Path path = operatorSettingsFile(); - long updatedTime = watchedFileTimestamp(path); - if (updatedTime > lastUpdatedTime) { - this.lastUpdatedTime = updatedTime; + if (logger.isDebugEnabled()) { + key.pollEvents().stream().forEach(e -> logger.debug("{}:{}", e.kind().toString(), e.context().toString())); + } + + key.pollEvents(); + key.reset(); + + enableSettingsWatcher(settingsDir); + + if (watchedFileChanged(path)) { processFileSettings(path); } } catch (IOException e) { logger.warn("unable to watch or read operator settings file", e); } } else { - key.cancel(); - enableSettingsWatcher(environment.configFile()); + key.pollEvents(); + key.reset(); } } - } catch (InterruptedException | ClosedWatchServiceException | IOException e) { + } catch (Exception e) { logger.debug("encountered exception watching, shutting down watcher thread.", e); } finally { watcherThreadLatch.countDown(); @@ -226,13 +241,15 @@ private void enableSettingsWatcher(Path settingsDir) throws IOException { } void processFileSettings(Path path) { - logger.info("Processing path [{}]", path); + logger.info("processing path [{}] for [{}]", path, NAMESPACE); try ( XContentParser parser = XContentType.JSON.xContent().createParser(XContentParserConfiguration.EMPTY, Files.newInputStream(path)) ) { - controller.process("operator", parser); + controller.process(NAMESPACE, parser); } catch (Exception e) { - logger.error("Error parsing operator settings json file", e); + logger.error("Error processing operator settings json file", e); } } + + record FileUpdateState(long timestamp, String path, Object fileKey) {} } diff --git a/server/src/main/java/org/elasticsearch/operator/service/OperatorClusterStateController.java b/server/src/main/java/org/elasticsearch/operator/service/OperatorClusterStateController.java index 80d38cb597a16..b9a4b2ac58b7a 100644 --- a/server/src/main/java/org/elasticsearch/operator/service/OperatorClusterStateController.java +++ b/server/src/main/java/org/elasticsearch/operator/service/OperatorClusterStateController.java @@ -27,6 +27,7 @@ import org.elasticsearch.xcontent.XContentParser; import java.io.IOException; +import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; @@ -81,19 +82,19 @@ static class SettingsFile { public ClusterState process(String namespace, XContentParser parser) throws IOException { SettingsFile operatorStateFileContent = SettingsFile.PARSER.apply(parser, null); - Map operatorState = operatorStateFileContent.state; OperatorStateVersionMetadata stateVersionMetadata = operatorStateFileContent.metadata; LinkedHashSet orderedHandlers = orderedStateHandlers(operatorState.keySet()); ClusterState state = clusterService.state(); - OperatorMetadata existingMetadata = state.metadata().operatorState(namespace); - - checkMetadataVersion(existingMetadata, stateVersionMetadata); + if (checkMetadataVersion(existingMetadata, stateVersionMetadata) == false) { + return state; + } OperatorMetadata.Builder operatorMetadataBuilder = new OperatorMetadata.Builder(namespace).version(stateVersionMetadata.version()); + List errors = new ArrayList<>(); for (var handlerKey : orderedHandlers) { OperatorHandler handler = handlers.get(handlerKey); @@ -103,11 +104,35 @@ public ClusterState process(String namespace, XContentParser parser) throws IOEx state = transformState.state(); operatorMetadataBuilder.putHandler(new OperatorHandlerMetadata.Builder(handlerKey).keys(transformState.keys()).build()); } catch (Exception e) { - // TODO: Collect all errors, store them in the cluster state metadata, throw at the end with all of them - throw new IllegalStateException("Error processing state change request for: " + handler.key(), e); + errors.add(String.format("Error processing %s state change: %s", handler.key(), e.getMessage())); } } + if (errors.isEmpty() == false) { + clusterService.submitStateUpdateTask("operator state error for [ " + namespace + "]", + new OperatorUpdateErrorTask(new ActionListener<>() { + @Override + public void onResponse(ActionResponse.Empty empty) { + logger.info("Successfully applied new operator error state for namespace [{}]", namespace); + } + + @Override + public void onFailure(Exception e) { + logger.error("Failed to apply operator error cluster state", e); + } + }), + ClusterStateTaskConfig.build(Priority.URGENT), + new OperatorUpdateErrorTask.OperatorUpdateErrorTaskExecutor(namespace, stateVersionMetadata.version(), errors) + ); + + logger.error("Error processing state change request for [{}] with the following errors [{}]", namespace, errors); + + throw new IllegalStateException("Error processing state change request for " + namespace); + } + + // remove the last error if we had previously encountered any + operatorMetadataBuilder.errorMetadata(null); + ClusterState.Builder stateBuilder = new ClusterState.Builder(state); Metadata.Builder metadataBuilder = Metadata.builder(state.metadata()).putOperatorState(operatorMetadataBuilder.build()); state = stateBuilder.metadata(metadataBuilder).build(); @@ -139,14 +164,24 @@ private Set keysForHandler(OperatorMetadata operatorMetadata, String han return operatorMetadata.handlers().get(handlerKey).keys(); } - void checkMetadataVersion(OperatorMetadata existingMetadata, OperatorStateVersionMetadata stateVersionMetadata) { + boolean checkMetadataVersion(OperatorMetadata existingMetadata, OperatorStateVersionMetadata stateVersionMetadata) { if (Version.CURRENT.before(stateVersionMetadata.minCompatibleVersion())) { - throw new IllegalStateException("Newer version operator cluster state"); + logger.info( + "Cluster state version [{}] is not compatible with this Elasticsearch node", + stateVersionMetadata.minCompatibleVersion() + ); + return false; } if (existingMetadata != null && existingMetadata.version() >= stateVersionMetadata.version()) { - throw new IllegalStateException("Cluster state not updated because version is less or equal to before"); + logger.info( + "Not updating cluster state because version [{}] is less or equal to the current metadata version [{}]", + stateVersionMetadata.version(), existingMetadata.version() + ); + return false; } + + return true; } LinkedHashSet orderedStateHandlers(Set keys) { diff --git a/server/src/main/java/org/elasticsearch/operator/service/OperatorUpdateErrorTask.java b/server/src/main/java/org/elasticsearch/operator/service/OperatorUpdateErrorTask.java new file mode 100644 index 0000000000000..1b55355225ef9 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/operator/service/OperatorUpdateErrorTask.java @@ -0,0 +1,74 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.operator.service; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.ActionResponse; +import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.ClusterStateTaskExecutor; +import org.elasticsearch.cluster.ClusterStateTaskListener; +import org.elasticsearch.cluster.metadata.Metadata; +import org.elasticsearch.cluster.metadata.OperatorErrorMetadata; +import org.elasticsearch.cluster.metadata.OperatorMetadata; + +import java.util.List; + +/** + * Cluster state update task that sets the error state of the operator metadata. + * This is used when an operator cluster state update encounters error(s) while processing + * the file. + * + * @param listener + */ +public record OperatorUpdateErrorTask(ActionListener listener) implements ClusterStateTaskListener { + private static final Logger logger = LogManager.getLogger(FileSettingsService.class); + + @Override + public void onFailure(Exception e) { + listener.onFailure(e); + } + + /** + * Operator update cluster state task executor + * + * @param namespace of the state we are updating + * @param version of the update that failed + * @param errors the list of errors to report + */ + public record OperatorUpdateErrorTaskExecutor(String namespace, Long version, List errors) + implements + ClusterStateTaskExecutor { + + @Override + public ClusterState execute(ClusterState currentState, List> taskContexts) throws Exception { + for (final var taskContext : taskContexts) { + taskContext.success( + taskContext.getTask().listener().delegateFailure((l, s) -> l.onResponse(ActionResponse.Empty.INSTANCE)) + ); + } + + ClusterState.Builder stateBuilder = new ClusterState.Builder(currentState); + Metadata.Builder metadataBuilder = Metadata.builder(currentState.metadata()); + OperatorMetadata operatorMetadata = currentState.metadata().operatorState(namespace); + OperatorMetadata.Builder operatorMetadataBuilder = OperatorMetadata.builder(namespace, operatorMetadata); + operatorMetadataBuilder.errorMetadata(OperatorErrorMetadata.builder().version(version).errors(errors).build()); + metadataBuilder.putOperatorState(operatorMetadataBuilder.build()); + ClusterState newState = stateBuilder.metadata(metadataBuilder).build(); + + return newState; + } + + @Override + public void clusterStatePublished(ClusterState newClusterState) { + logger.info("Wrote new error state in operator metadata"); + } + } +} From f69562ebc706386f3b1eb78ec0bea7a14764a308 Mon Sep 17 00:00:00 2001 From: Nikola Grcevski Date: Fri, 3 Jun 2022 11:29:50 -0400 Subject: [PATCH 18/48] Fix tests --- .../metadata/OperatorErrorMetadata.java | 3 +- .../cluster/metadata/OperatorMetadata.java | 3 +- .../operator/service/FileSettingsService.java | 26 +++++++--------- .../OperatorClusterStateController.java | 31 +++++++++++++------ .../reroute/ClusterRerouteResponseTests.java | 6 ++-- .../cluster/ClusterStateTests.java | 12 ++++--- .../metadata/ToAndFromJsonMetadataTests.java | 15 ++++++--- 7 files changed, 58 insertions(+), 38 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/OperatorErrorMetadata.java b/server/src/main/java/org/elasticsearch/cluster/metadata/OperatorErrorMetadata.java index 7b65c0c3cfc54..b5110b1f8819f 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/OperatorErrorMetadata.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/OperatorErrorMetadata.java @@ -100,8 +100,7 @@ public OperatorErrorMetadata build() { * @param builder * @param params */ - public static void toXContent(OperatorErrorMetadata metadata, XContentBuilder builder, Params params) - throws IOException { + public static void toXContent(OperatorErrorMetadata metadata, XContentBuilder builder, Params params) throws IOException { builder.startObject(); builder.field(VERSION, metadata.version); builder.stringListField(ERRORS, metadata.errors); diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/OperatorMetadata.java b/server/src/main/java/org/elasticsearch/cluster/metadata/OperatorMetadata.java index 75dd75d51b12b..69690b50e6d48 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/OperatorMetadata.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/OperatorMetadata.java @@ -32,7 +32,8 @@ public OperatorMetadata( String namespace, Long version, Map handlers, - OperatorErrorMetadata errorMetadata) { + OperatorErrorMetadata errorMetadata + ) { this.namespace = namespace; this.version = version; this.handlers = handlers; diff --git a/server/src/main/java/org/elasticsearch/operator/service/FileSettingsService.java b/server/src/main/java/org/elasticsearch/operator/service/FileSettingsService.java index b8b399fb4ac81..2b37258e1e3d0 100644 --- a/server/src/main/java/org/elasticsearch/operator/service/FileSettingsService.java +++ b/server/src/main/java/org/elasticsearch/operator/service/FileSettingsService.java @@ -80,11 +80,7 @@ boolean watchedFileChanged(Path path) throws IOException { FileUpdateState previousUpdateState = fileUpdateState; BasicFileAttributes attr = Files.readAttributes(path, BasicFileAttributes.class); - fileUpdateState = new FileUpdateState( - attr.lastModifiedTime().toMillis(), - path.toRealPath().toString(), - attr.fileKey() - ); + fileUpdateState = new FileUpdateState(attr.lastModifiedTime().toMillis(), path.toRealPath().toString(), attr.fileKey()); return (previousUpdateState == null || previousUpdateState.equals(fileUpdateState) == false); } @@ -173,15 +169,17 @@ synchronized void startWatcher() { WatchKey key; while ((key = watchService.take()) != null) { - // Reading and interpreting watch service events can vary from platform to platform. E.g: - // MacOS symlink delete and set (rm -rf operator && ln -s /file_settings/ operator): - // ENTRY_MODIFY:operator - // ENTRY_CREATE:settings.json - // ENTRY_MODIFY:settings.json - // Linux in Docker symlink delete and set (rm -rf operator && ln -s /file_settings/ operator): - // ENTRY_CREATE:operator - // After we get an indication that something has changed, we check the timestamp, file id, - // real path of our desired file. + /** + * Reading and interpreting watch service events can vary from platform to platform. E.g: + * MacOS symlink delete and set (rm -rf operator && ln -s /file_settings/ operator): + * ENTRY_MODIFY:operator + * ENTRY_CREATE:settings.json + * ENTRY_MODIFY:settings.json + * Linux in Docker symlink delete and set (rm -rf operator && ln -s /file_settings/ operator): + * ENTRY_CREATE:operator + * After we get an indication that something has changed, we check the timestamp, file id, + * real path of our desired file. + */ if (Files.exists(settingsDir)) { try { Path path = operatorSettingsFile(); diff --git a/server/src/main/java/org/elasticsearch/operator/service/OperatorClusterStateController.java b/server/src/main/java/org/elasticsearch/operator/service/OperatorClusterStateController.java index b9a4b2ac58b7a..fc297611e5224 100644 --- a/server/src/main/java/org/elasticsearch/operator/service/OperatorClusterStateController.java +++ b/server/src/main/java/org/elasticsearch/operator/service/OperatorClusterStateController.java @@ -26,7 +26,6 @@ import org.elasticsearch.xcontent.ParseField; import org.elasticsearch.xcontent.XContentParser; -import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashSet; @@ -36,6 +35,8 @@ import java.util.function.Function; import java.util.stream.Collectors; +import static org.elasticsearch.core.Strings.format; + /** * Controller class for applying file based settings to ClusterState. * This class contains the logic about validation, ordering and applying of @@ -80,7 +81,15 @@ static class SettingsFile { } } - public ClusterState process(String namespace, XContentParser parser) throws IOException { + /** + * Saves an operator cluster state for a given 'namespace' from XContentParser + * + * @param namespace the namespace under which we'll store the operator keys in the cluster state metadata + * @param parser the XContentParser to process + * @return the modified cluster state. If applying the cluster state fails the previous state might be returned. + * @throws IllegalStateException if the content has errors and the cluster state cannot be correctly applied + */ + public ClusterState process(String namespace, XContentParser parser) { SettingsFile operatorStateFileContent = SettingsFile.PARSER.apply(parser, null); Map operatorState = operatorStateFileContent.state; OperatorStateVersionMetadata stateVersionMetadata = operatorStateFileContent.metadata; @@ -104,12 +113,13 @@ public ClusterState process(String namespace, XContentParser parser) throws IOEx state = transformState.state(); operatorMetadataBuilder.putHandler(new OperatorHandlerMetadata.Builder(handlerKey).keys(transformState.keys()).build()); } catch (Exception e) { - errors.add(String.format("Error processing %s state change: %s", handler.key(), e.getMessage())); + errors.add(format("Error processing %s state change: %s", handler.key(), e.getMessage())); } } if (errors.isEmpty() == false) { - clusterService.submitStateUpdateTask("operator state error for [ " + namespace + "]", + clusterService.submitStateUpdateTask( + "operator state error for [ " + namespace + "]", new OperatorUpdateErrorTask(new ActionListener<>() { @Override public void onResponse(ActionResponse.Empty empty) { @@ -166,17 +176,18 @@ private Set keysForHandler(OperatorMetadata operatorMetadata, String han boolean checkMetadataVersion(OperatorMetadata existingMetadata, OperatorStateVersionMetadata stateVersionMetadata) { if (Version.CURRENT.before(stateVersionMetadata.minCompatibleVersion())) { - logger.info( - "Cluster state version [{}] is not compatible with this Elasticsearch node", - stateVersionMetadata.minCompatibleVersion() - ); - return false; + logger.info( + "Cluster state version [{}] is not compatible with this Elasticsearch node", + stateVersionMetadata.minCompatibleVersion() + ); + return false; } if (existingMetadata != null && existingMetadata.version() >= stateVersionMetadata.version()) { logger.info( "Not updating cluster state because version [{}] is less or equal to the current metadata version [{}]", - stateVersionMetadata.version(), existingMetadata.version() + stateVersionMetadata.version(), + existingMetadata.version() ); return false; } diff --git a/server/src/test/java/org/elasticsearch/action/admin/cluster/reroute/ClusterRerouteResponseTests.java b/server/src/test/java/org/elasticsearch/action/admin/cluster/reroute/ClusterRerouteResponseTests.java index 332233d2b7d66..b9ee982a1f41e 100644 --- a/server/src/test/java/org/elasticsearch/action/admin/cluster/reroute/ClusterRerouteResponseTests.java +++ b/server/src/test/java/org/elasticsearch/action/admin/cluster/reroute/ClusterRerouteResponseTests.java @@ -142,7 +142,8 @@ public void testToXContent() throws IOException { }, "index-graveyard": { "tombstones": [] - } + }, + "operator":{} }, "routing_table": { "indices": {} @@ -246,7 +247,8 @@ public void testToXContent() throws IOException { }, "index-graveyard" : { "tombstones" : [ ] - } + }, + "operator":{} } } }"""), XContentHelper.stripWhitespace(Strings.toString(builder))); diff --git a/server/src/test/java/org/elasticsearch/cluster/ClusterStateTests.java b/server/src/test/java/org/elasticsearch/cluster/ClusterStateTests.java index 5a93fa16359c0..05d417bef0077 100644 --- a/server/src/test/java/org/elasticsearch/cluster/ClusterStateTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/ClusterStateTests.java @@ -286,7 +286,8 @@ public void testToXContent() throws IOException { }, "index-graveyard": { "tombstones": [] - } + }, + "operator" : { } }, "routing_table": { "indices": { @@ -489,7 +490,8 @@ public void testToXContent_FlatSettingTrue_ReduceMappingFalse() throws IOExcepti }, "index-graveyard" : { "tombstones" : [ ] - } + }, + "operator" : { } }, "routing_table" : { "indices" : { @@ -699,7 +701,8 @@ public void testToXContent_FlatSettingFalse_ReduceMappingTrue() throws IOExcepti }, "index-graveyard" : { "tombstones" : [ ] - } + }, + "operator" : { } }, "routing_table" : { "indices" : { @@ -840,7 +843,8 @@ public void testToXContentSameTypeName() throws IOException { }, "index-graveyard" : { "tombstones" : [ ] - } + }, + "operator" : { } }, "routing_table" : { "indices" : { } diff --git a/server/src/test/java/org/elasticsearch/cluster/metadata/ToAndFromJsonMetadataTests.java b/server/src/test/java/org/elasticsearch/cluster/metadata/ToAndFromJsonMetadataTests.java index 77ccb5bf93db5..3ab133525ccb1 100644 --- a/server/src/test/java/org/elasticsearch/cluster/metadata/ToAndFromJsonMetadataTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/metadata/ToAndFromJsonMetadataTests.java @@ -246,7 +246,8 @@ public void testToXContentGateway_FlatSettingTrue_ReduceMappingFalse() throws IO }, "index-graveyard" : { "tombstones" : [ ] - } + }, + "operator" : { } } }""".formatted(Version.CURRENT.id, Version.CURRENT.id), Strings.toString(builder)); } @@ -341,7 +342,8 @@ public void testToXContentAPI_SameTypeName() throws IOException { }, "index-graveyard" : { "tombstones" : [ ] - } + }, + "operator" : { } } }""".formatted(Version.CURRENT.id), Strings.toString(builder)); } @@ -405,7 +407,8 @@ public void testToXContentGateway_FlatSettingFalse_ReduceMappingTrue() throws IO }, "index-graveyard" : { "tombstones" : [ ] - } + }, + "operator" : { } } }""".formatted(Version.CURRENT.id, Version.CURRENT.id), Strings.toString(builder)); } @@ -507,7 +510,8 @@ public void testToXContentAPI_FlatSettingTrue_ReduceMappingFalse() throws IOExce }, "index-graveyard" : { "tombstones" : [ ] - } + }, + "operator" : { } } }""".formatted(Version.CURRENT.id, Version.CURRENT.id), Strings.toString(builder)); } @@ -615,7 +619,8 @@ public void testToXContentAPI_FlatSettingFalse_ReduceMappingTrue() throws IOExce }, "index-graveyard" : { "tombstones" : [ ] - } + }, + "operator" : { } } }""".formatted(Version.CURRENT.id, Version.CURRENT.id), Strings.toString(builder)); } From 6a80673469c87f49965ea4d4257260e575fe9aa3 Mon Sep 17 00:00:00 2001 From: Nikola Grcevski Date: Fri, 3 Jun 2022 11:46:48 -0400 Subject: [PATCH 19/48] Fix bwc --- .../elasticsearch/cluster/metadata/Metadata.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/Metadata.java b/server/src/main/java/org/elasticsearch/cluster/metadata/Metadata.java index 45ef0351e82a4..d97df34a9040f 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/Metadata.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/Metadata.java @@ -1207,11 +1207,12 @@ public static Metadata readFrom(StreamInput in) throws IOException { Custom customIndexMetadata = in.readNamedWriteable(Custom.class); builder.putCustom(customIndexMetadata.getWriteableName(), customIndexMetadata); } - size = in.readVInt(); - for (int i = 0; i < size; i++) { - builder.putOperatorState(OperatorMetadata.readFrom(in)); + if (in.getVersion().onOrAfter(Version.V_8_4_0)) { + int operatorSize = in.readVInt(); + for (int i = 0; i < operatorSize; i++) { + builder.putOperatorState(OperatorMetadata.readFrom(in)); + } } - return builder.build(); } @@ -1238,7 +1239,9 @@ public void writeTo(StreamOutput out) throws IOException { } out.writeCollection(templates.values()); VersionedNamedWriteable.writeVersionedWritables(out, customs); - out.writeCollection(operatorState.values()); + if (out.getVersion().onOrAfter(Version.V_8_4_0)) { + out.writeCollection(operatorState.values()); + } } public static Builder builder() { From ba72b137f16fe2aa7c01b8ddfff3133bed1032fe Mon Sep 17 00:00:00 2001 From: Nikola Grcevski Date: Fri, 3 Jun 2022 14:46:43 -0400 Subject: [PATCH 20/48] More bwc fixes --- .../elasticsearch/cluster/metadata/Metadata.java | 14 ++++++++++++-- .../cluster/metadata/OperatorMetadata.java | 11 +++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/Metadata.java b/server/src/main/java/org/elasticsearch/cluster/metadata/Metadata.java index d97df34a9040f..7647e759284bf 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/Metadata.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/Metadata.java @@ -1130,7 +1130,15 @@ private static class MetadataDiff implements Diff { indices = DiffableUtils.readImmutableOpenMapDiff(in, DiffableUtils.getStringKeySerializer(), INDEX_METADATA_DIFF_VALUE_READER); templates = DiffableUtils.readImmutableOpenMapDiff(in, DiffableUtils.getStringKeySerializer(), TEMPLATES_DIFF_VALUE_READER); customs = DiffableUtils.readImmutableOpenMapDiff(in, DiffableUtils.getStringKeySerializer(), CUSTOM_VALUE_SERIALIZER); - operatorState = DiffableUtils.readImmutableOpenMapDiff(in, DiffableUtils.getStringKeySerializer(), OPERATOR_DIFF_VALUE_READER); + if (in.getVersion().onOrAfter(Version.V_8_4_0)) { + operatorState = DiffableUtils.readImmutableOpenMapDiff( + in, + DiffableUtils.getStringKeySerializer(), + OPERATOR_DIFF_VALUE_READER + ); + } else { + operatorState = OperatorMetadata.EMPTY_DIFF; + } } @Override @@ -1147,7 +1155,9 @@ public void writeTo(StreamOutput out) throws IOException { indices.writeTo(out); templates.writeTo(out); customs.writeTo(out); - operatorState.writeTo(out); + if (out.getVersion().onOrAfter(Version.V_8_4_0)) { + operatorState.writeTo(out); + } } @Override diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/OperatorMetadata.java b/server/src/main/java/org/elasticsearch/cluster/metadata/OperatorMetadata.java index 69690b50e6d48..070638c67b108 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/OperatorMetadata.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/OperatorMetadata.java @@ -10,7 +10,9 @@ import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.cluster.Diff; +import org.elasticsearch.cluster.DiffableUtils; import org.elasticsearch.cluster.SimpleDiffable; +import org.elasticsearch.common.collect.ImmutableOpenMap; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.xcontent.ToXContent; @@ -20,6 +22,7 @@ import java.io.IOException; import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; public class OperatorMetadata implements SimpleDiffable { @@ -81,6 +84,14 @@ public static Diff readDiffFrom(StreamInput in) throws IOExcep return SimpleDiffable.readDiffFrom(OperatorMetadata::readFrom, in); } + public static final DiffableUtils.MapDiff> EMPTY_DIFF = + new DiffableUtils.MapDiff<>(null, null, List.of(), List.of(), List.of()) { + @Override + public ImmutableOpenMap apply(ImmutableOpenMap part) { + return part; + } + }; + public static Builder builder(String namespace) { return new Builder(namespace); } From 0b626e32345bfde3b9b93c48c0e1a22752d70665 Mon Sep 17 00:00:00 2001 From: Nikola Grcevski Date: Fri, 3 Jun 2022 15:06:06 -0400 Subject: [PATCH 21/48] Undo unnecessary changes --- .../action/admin/cluster/RestClusterUpdateSettingsAction.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestClusterUpdateSettingsAction.java b/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestClusterUpdateSettingsAction.java index d2cb9b320a671..21ad55b4684ec 100644 --- a/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestClusterUpdateSettingsAction.java +++ b/server/src/main/java/org/elasticsearch/rest/action/admin/cluster/RestClusterUpdateSettingsAction.java @@ -25,8 +25,8 @@ import static org.elasticsearch.rest.RestRequest.Method.PUT; public class RestClusterUpdateSettingsAction extends BaseRestHandler { - public static final String PERSISTENT = "persistent"; - public static final String TRANSIENT = "transient"; + private static final String PERSISTENT = "persistent"; + private static final String TRANSIENT = "transient"; @Override public List routes() { From 4048a3c78d8c9d282e6066e5e88d45f25b18f2e6 Mon Sep 17 00:00:00 2001 From: Nikola Grcevski Date: Mon, 6 Jun 2022 09:42:00 -0400 Subject: [PATCH 22/48] switch to jdk Map --- .../cluster/metadata/Metadata.java | 22 ++++++++----------- .../cluster/metadata/OperatorMetadata.java | 5 ++--- 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/Metadata.java b/server/src/main/java/org/elasticsearch/cluster/metadata/Metadata.java index 7647e759284bf..2e4d7396ad4a7 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/Metadata.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/Metadata.java @@ -207,7 +207,7 @@ default boolean isRestorable() { private final ImmutableOpenMap> aliasedIndices; private final ImmutableOpenMap templates; private final ImmutableOpenMap customs; - private final ImmutableOpenMap operatorState; + private final Map operatorState; private final transient int totalNumberOfShards; // Transient ? not serializable anyway? private final int totalOpenIndexShards; @@ -248,7 +248,7 @@ private Metadata( SortedMap indicesLookup, Map mappingsByHash, Version oldestIndexVersion, - ImmutableOpenMap operatorState + Map operatorState ) { this.clusterUUID = clusterUUID; this.clusterUUIDCommitted = clusterUUIDCommitted; @@ -1092,7 +1092,7 @@ private static class MetadataDiff implements Diff { private final Diff> indices; private final Diff> templates; private final Diff> customs; - private final Diff> operatorState; + private final Diff> operatorState; MetadataDiff(Metadata before, Metadata after) { clusterUUID = after.clusterUUID; @@ -1131,11 +1131,7 @@ private static class MetadataDiff implements Diff { templates = DiffableUtils.readImmutableOpenMapDiff(in, DiffableUtils.getStringKeySerializer(), TEMPLATES_DIFF_VALUE_READER); customs = DiffableUtils.readImmutableOpenMapDiff(in, DiffableUtils.getStringKeySerializer(), CUSTOM_VALUE_SERIALIZER); if (in.getVersion().onOrAfter(Version.V_8_4_0)) { - operatorState = DiffableUtils.readImmutableOpenMapDiff( - in, - DiffableUtils.getStringKeySerializer(), - OPERATOR_DIFF_VALUE_READER - ); + operatorState = DiffableUtils.readJdkMapDiff(in, DiffableUtils.getStringKeySerializer(), OPERATOR_DIFF_VALUE_READER); } else { operatorState = OperatorMetadata.EMPTY_DIFF; } @@ -1286,7 +1282,7 @@ public static class Builder { private SortedMap previousIndicesLookup; - private final ImmutableOpenMap.Builder operatorState; + private final Map operatorState; // If this is set to false we can skip checking #mappingsByHash for unused entries in #build(). Used as an optimization to save // the rather expensive call to #purgeUnusedEntries when building from another instance and we know that no mappings can have @@ -1315,7 +1311,7 @@ public Builder() { this.previousIndicesLookup = metadata.indicesLookup; this.mappingsByHash = new HashMap<>(metadata.mappingsByHash); this.checkForUnusedMappings = false; - this.operatorState = ImmutableOpenMap.builder(metadata.operatorState); + this.operatorState = new HashMap<>(metadata.operatorState); } private Builder(Map mappingsByHash) { @@ -1324,7 +1320,7 @@ private Builder(Map mappingsByHash) { aliasedIndices = ImmutableOpenMap.builder(); templates = ImmutableOpenMap.builder(); customs = ImmutableOpenMap.builder(); - operatorState = ImmutableOpenMap.builder(); + operatorState = new HashMap<>(); indexGraveyard(IndexGraveyard.builder().build()); // create new empty index graveyard to initialize previousIndicesLookup = null; this.mappingsByHash = new HashMap<>(mappingsByHash); @@ -1682,7 +1678,7 @@ public OperatorMetadata operatorState(String namespace) { } public Builder operatorState(Map operatorState) { - this.operatorState.putAllFromMap(operatorState); + this.operatorState.putAll(operatorState); return this; } @@ -1896,7 +1892,7 @@ public Metadata build() { indicesLookup, Collections.unmodifiableMap(mappingsByHash), Version.fromId(oldestIndexVersionId), - operatorState.build() + Collections.unmodifiableMap(operatorState) ); } diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/OperatorMetadata.java b/server/src/main/java/org/elasticsearch/cluster/metadata/OperatorMetadata.java index 070638c67b108..eddaa62d124e0 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/OperatorMetadata.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/OperatorMetadata.java @@ -12,7 +12,6 @@ import org.elasticsearch.cluster.Diff; import org.elasticsearch.cluster.DiffableUtils; import org.elasticsearch.cluster.SimpleDiffable; -import org.elasticsearch.common.collect.ImmutableOpenMap; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.xcontent.ToXContent; @@ -84,10 +83,10 @@ public static Diff readDiffFrom(StreamInput in) throws IOExcep return SimpleDiffable.readDiffFrom(OperatorMetadata::readFrom, in); } - public static final DiffableUtils.MapDiff> EMPTY_DIFF = + public static final DiffableUtils.MapDiff> EMPTY_DIFF = new DiffableUtils.MapDiff<>(null, null, List.of(), List.of(), List.of()) { @Override - public ImmutableOpenMap apply(ImmutableOpenMap part) { + public Map apply(Map part) { return part; } }; From 131dd0774f54e8ab56056d40d2055f4ea65b5e0e Mon Sep 17 00:00:00 2001 From: Nikola Grcevski Date: Mon, 6 Jun 2022 10:35:55 -0400 Subject: [PATCH 23/48] Extend metadata with error kind --- .../metadata/OperatorErrorMetadata.java | 70 +++++++++++++++++-- .../cluster/metadata/OperatorMetadata.java | 31 +++++++- .../OperatorClusterStateController.java | 57 ++++++++++----- .../service/OperatorUpdateErrorTask.java | 13 ++-- .../OperatorClusterStateControllerTests.java | 2 +- 5 files changed, 142 insertions(+), 31 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/OperatorErrorMetadata.java b/server/src/main/java/org/elasticsearch/cluster/metadata/OperatorErrorMetadata.java index b5110b1f8819f..7a7dd406da1bc 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/OperatorErrorMetadata.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/OperatorErrorMetadata.java @@ -22,16 +22,26 @@ import java.util.List; /** - * Metadata class to hold the operator set keys for each operator handler + * Metadata class to hold error information about errors encountered + * while applying a cluster state update for a given namespace. * + * This information is held by the OperatorMetadata class. */ public class OperatorErrorMetadata implements SimpleDiffable, ToXContentFragment { private final Long version; + private final ErrorKind errorKind; private final List errors; - public OperatorErrorMetadata(Long version, List errors) { + /** + * Contructs an operator metadata + * @param version the metadata version which failed to apply + * @param errorKind the kind of error we encountered while processing + * @param errors the list of errors encountered during parsing and validation of the metadata + */ + public OperatorErrorMetadata(Long version, ErrorKind errorKind, List errors) { this.version = version; this.errors = errors; + this.errorKind = errorKind; } public Long version() { @@ -42,14 +52,21 @@ public List errors() { return this.errors; } + public ErrorKind errorKind() { + return errorKind; + } + @Override public void writeTo(StreamOutput out) throws IOException { out.writeLong(version); + out.writeString(errorKind.getKindValue()); out.writeCollection(errors, StreamOutput::writeString); } public static OperatorErrorMetadata readFrom(StreamInput in) throws IOException { - Builder builder = new Builder().version(in.readLong()).errors(in.readList(StreamInput::readString)); + Builder builder = new Builder().version(in.readLong()) + .errorKind(ErrorKind.of(in.readString())) + .errors(in.readList(StreamInput::readString)); return builder.build(); } @@ -67,12 +84,17 @@ public static Builder builder() { return new Builder(); } + /** + * Builder class for the OperatorErrorMetadata + */ public static class Builder { private static final String ERRORS = "errors"; private static final String VERSION = "version"; + private static final String ERROR_KIND = "error_kind"; private Long version; private List errors; + private ErrorKind errorKind; public Builder() { this.version = 0L; @@ -89,12 +111,17 @@ public Builder errors(List errors) { return this; } + public Builder errorKind(ErrorKind errorKind) { + this.errorKind = errorKind; + return this; + } + public OperatorErrorMetadata build() { - return new OperatorErrorMetadata(version, Collections.unmodifiableList(errors)); + return new OperatorErrorMetadata(version, errorKind, Collections.unmodifiableList(errors)); } /** - * Serializes the metadata to xContent + * Serializes the error metadata to xContent * * @param metadata * @param builder @@ -103,12 +130,13 @@ public OperatorErrorMetadata build() { public static void toXContent(OperatorErrorMetadata metadata, XContentBuilder builder, Params params) throws IOException { builder.startObject(); builder.field(VERSION, metadata.version); + builder.field(ERROR_KIND, metadata.errorKind.getKindValue()); builder.stringListField(ERRORS, metadata.errors); builder.endObject(); } /** - * Reads the metadata from xContent + * Reads the error metadata from xContent * * @param parser * @return @@ -133,10 +161,40 @@ public static OperatorErrorMetadata fromXContent(XContentParser parser) throws I } else if (token.isValue()) { if (VERSION.equals(currentFieldName)) { builder.version(parser.longValue()); + } else if (ERROR_KIND.equals(currentFieldName)) { + builder.errorKind(ErrorKind.of(parser.text())); } } } return builder.build(); } } + + /** + * Enum for kinds of errors we might encounter while processing operator cluster state updates. + */ + public enum ErrorKind { + PARSING("parsing"), + VALIDATION("validation"), + TRANSIENT("transient"); + + private final String kind; + + ErrorKind(String kind) { + this.kind = kind; + } + + public String getKindValue() { + return kind; + } + + public static ErrorKind of(String kind) { + for (var report : values()) { + if (report.kind.equals(kind)) { + return report; + } + } + throw new IllegalArgumentException("kind not supported [" + kind + "]"); + } + } } diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/OperatorMetadata.java b/server/src/main/java/org/elasticsearch/cluster/metadata/OperatorMetadata.java index eddaa62d124e0..a0c030f0acd14 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/OperatorMetadata.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/OperatorMetadata.java @@ -24,12 +24,27 @@ import java.util.List; import java.util.Map; +/** + * Metadata class that contains information about cluster settings/entities set + * in an operator mode. These types of settings are read only through the REST API, + * and cannot be modified by the end user. + */ public class OperatorMetadata implements SimpleDiffable { private final String namespace; private final Long version; private final Map handlers; private final OperatorErrorMetadata errorMetadata; + /** + * OperatorMetadata contains information about settings set in operator mode. + * These settings cannot be updated by the end user and are set outside of the + * REST layer, e.g. through file based settings or by plugin/modules. + * + * @param namespace The namespace of the setting creator, e.g. file_settings, security plugin, etc. + * @param version The update version, must increase with each update + * @param handlers Per state update handler information on key set in by this update. These keys are validated at REST time. + * @param errorMetadata If the update failed for some reason, this is where we store the error information metadata. + */ public OperatorMetadata( String namespace, Long version, @@ -99,6 +114,9 @@ public static Builder builder(String namespace, OperatorMetadata metadata) { return new Builder(namespace, metadata); } + /** + * Builder class for OperatorMetadata + */ public static class Builder { private static final String VERSION = "version"; private static final String HANDLERS = "handlers"; @@ -109,6 +127,10 @@ public static class Builder { private Map handlers; OperatorErrorMetadata errorMetadata; + /** + * Empty builder for OperatorMetadata + * @param namespace The namespace for this metadata + */ public Builder(String namespace) { this.namespace = namespace; this.version = 0L; @@ -116,6 +138,11 @@ public Builder(String namespace) { this.errorMetadata = null; } + /** + * Creates an operator metadata builder + * @param namespace the namespace for which we are storing metadata, e.g. file_settings + * @param metadata the previous metadata + */ public Builder(String namespace, OperatorMetadata metadata) { this(namespace); if (metadata != null) { @@ -150,7 +177,7 @@ public OperatorMetadata build() { } /** - * Serializes the metadata to xContent + * Serializes the operator metadata to xContent * * @param operatorMetadata * @param builder @@ -170,7 +197,7 @@ public static void toXContent(OperatorMetadata operatorMetadata, XContentBuilder } /** - * Reads the metadata from xContent + * Reads the operator metadata from xContent * * @param parser * @param namespace diff --git a/server/src/main/java/org/elasticsearch/operator/service/OperatorClusterStateController.java b/server/src/main/java/org/elasticsearch/operator/service/OperatorClusterStateController.java index fc297611e5224..3da643f44996c 100644 --- a/server/src/main/java/org/elasticsearch/operator/service/OperatorClusterStateController.java +++ b/server/src/main/java/org/elasticsearch/operator/service/OperatorClusterStateController.java @@ -16,6 +16,7 @@ import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.ClusterStateTaskConfig; import org.elasticsearch.cluster.metadata.Metadata; +import org.elasticsearch.cluster.metadata.OperatorErrorMetadata; import org.elasticsearch.cluster.metadata.OperatorHandlerMetadata; import org.elasticsearch.cluster.metadata.OperatorMetadata; import org.elasticsearch.cluster.service.ClusterService; @@ -90,7 +91,18 @@ static class SettingsFile { * @throws IllegalStateException if the content has errors and the cluster state cannot be correctly applied */ public ClusterState process(String namespace, XContentParser parser) { - SettingsFile operatorStateFileContent = SettingsFile.PARSER.apply(parser, null); + SettingsFile operatorStateFileContent = null; + + try { + operatorStateFileContent = SettingsFile.PARSER.apply(parser, null); + } catch (Exception e) { + List errors = List.of(e.getMessage()); + recordErrorState(namespace, -1L, errors, OperatorErrorMetadata.ErrorKind.PARSING); + logger.error("Error processing state change request for [{}] with the following errors [{}]", namespace, errors); + + throw new IllegalStateException("Error processing state change request for " + namespace, e); + } + Map operatorState = operatorStateFileContent.state; OperatorStateVersionMetadata stateVersionMetadata = operatorStateFileContent.metadata; @@ -118,23 +130,7 @@ public ClusterState process(String namespace, XContentParser parser) { } if (errors.isEmpty() == false) { - clusterService.submitStateUpdateTask( - "operator state error for [ " + namespace + "]", - new OperatorUpdateErrorTask(new ActionListener<>() { - @Override - public void onResponse(ActionResponse.Empty empty) { - logger.info("Successfully applied new operator error state for namespace [{}]", namespace); - } - - @Override - public void onFailure(Exception e) { - logger.error("Failed to apply operator error cluster state", e); - } - }), - ClusterStateTaskConfig.build(Priority.URGENT), - new OperatorUpdateErrorTask.OperatorUpdateErrorTaskExecutor(namespace, stateVersionMetadata.version(), errors) - ); - + recordErrorState(namespace, stateVersionMetadata.version(), errors, OperatorErrorMetadata.ErrorKind.VALIDATION); logger.error("Error processing state change request for [{}] with the following errors [{}]", namespace, errors); throw new IllegalStateException("Error processing state change request for " + namespace); @@ -157,6 +153,12 @@ public void onResponse(ActionResponse.Empty empty) { @Override public void onFailure(Exception e) { logger.error("Failed to apply operator cluster state", e); + recordErrorState( + namespace, + stateVersionMetadata.version(), + List.of(e.getMessage()), + OperatorErrorMetadata.ErrorKind.TRANSIENT + ); } }), ClusterStateTaskConfig.build(Priority.URGENT), @@ -195,6 +197,25 @@ boolean checkMetadataVersion(OperatorMetadata existingMetadata, OperatorStateVer return true; } + void recordErrorState(String namespace, Long version, List errors, OperatorErrorMetadata.ErrorKind errorKind) { + clusterService.submitStateUpdateTask( + "operator state error for [ " + namespace + "]", + new OperatorUpdateErrorTask(new ActionListener<>() { + @Override + public void onResponse(ActionResponse.Empty empty) { + logger.info("Successfully applied new operator error state for namespace [{}]", namespace); + } + + @Override + public void onFailure(Exception e) { + logger.error("Failed to apply operator error cluster state", e); + } + }), + ClusterStateTaskConfig.build(Priority.URGENT), + new OperatorUpdateErrorTask.OperatorUpdateErrorTaskExecutor(namespace, version, errorKind, errors) + ); + } + LinkedHashSet orderedStateHandlers(Set keys) { LinkedHashSet orderedHandlers = new LinkedHashSet<>(); LinkedHashSet dependencyStack = new LinkedHashSet<>(); diff --git a/server/src/main/java/org/elasticsearch/operator/service/OperatorUpdateErrorTask.java b/server/src/main/java/org/elasticsearch/operator/service/OperatorUpdateErrorTask.java index 1b55355225ef9..1bfe1dcd53fc0 100644 --- a/server/src/main/java/org/elasticsearch/operator/service/OperatorUpdateErrorTask.java +++ b/server/src/main/java/org/elasticsearch/operator/service/OperatorUpdateErrorTask.java @@ -43,9 +43,12 @@ public void onFailure(Exception e) { * @param version of the update that failed * @param errors the list of errors to report */ - public record OperatorUpdateErrorTaskExecutor(String namespace, Long version, List errors) - implements - ClusterStateTaskExecutor { + public record OperatorUpdateErrorTaskExecutor( + String namespace, + Long version, + OperatorErrorMetadata.ErrorKind errorKind, + List errors + ) implements ClusterStateTaskExecutor { @Override public ClusterState execute(ClusterState currentState, List> taskContexts) throws Exception { @@ -59,7 +62,9 @@ public ClusterState execute(ClusterState currentState, List Date: Mon, 6 Jun 2022 10:41:45 -0400 Subject: [PATCH 24/48] Add more tests --- .../OperatorClusterStateControllerTests.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/server/src/test/java/org/elasticsearch/operator/OperatorClusterStateControllerTests.java b/server/src/test/java/org/elasticsearch/operator/OperatorClusterStateControllerTests.java index 667df7f187bb2..e810089f49736 100644 --- a/server/src/test/java/org/elasticsearch/operator/OperatorClusterStateControllerTests.java +++ b/server/src/test/java/org/elasticsearch/operator/OperatorClusterStateControllerTests.java @@ -53,6 +53,27 @@ public void testOperatorController() throws IOException { } """; + try (XContentParser parser = XContentType.JSON.xContent().createParser(XContentParserConfiguration.EMPTY, testJSON)) { + assertEquals( + "Error processing state change request for operator", + expectThrows(IllegalStateException.class, () -> controller.process("operator", parser)).getMessage() + ); + } + + testJSON = """ + { + "metadata": { + "version": "1234", + "compatibility": "8.4.0" + }, + "state": { + "cluster_settings": { + "indices.recovery.max_bytes_per_sec": "50mb" + } + } + } + """; + try (XContentParser parser = XContentType.JSON.xContent().createParser(XContentParserConfiguration.EMPTY, testJSON)) { controller.process("operator", parser); } From 9e725e9f771397820e598fc75d3e60eccbce8b7d Mon Sep 17 00:00:00 2001 From: Nikola Grcevski Date: Mon, 6 Jun 2022 10:46:54 -0400 Subject: [PATCH 25/48] Add more error handling --- .../service/OperatorClusterStateController.java | 13 +++++++++++-- .../action/operator/OperatorILMControllerTests.java | 2 +- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/operator/service/OperatorClusterStateController.java b/server/src/main/java/org/elasticsearch/operator/service/OperatorClusterStateController.java index 3da643f44996c..3e4f41a72ca25 100644 --- a/server/src/main/java/org/elasticsearch/operator/service/OperatorClusterStateController.java +++ b/server/src/main/java/org/elasticsearch/operator/service/OperatorClusterStateController.java @@ -91,7 +91,7 @@ static class SettingsFile { * @throws IllegalStateException if the content has errors and the cluster state cannot be correctly applied */ public ClusterState process(String namespace, XContentParser parser) { - SettingsFile operatorStateFileContent = null; + SettingsFile operatorStateFileContent; try { operatorStateFileContent = SettingsFile.PARSER.apply(parser, null); @@ -106,7 +106,16 @@ public ClusterState process(String namespace, XContentParser parser) { Map operatorState = operatorStateFileContent.state; OperatorStateVersionMetadata stateVersionMetadata = operatorStateFileContent.metadata; - LinkedHashSet orderedHandlers = orderedStateHandlers(operatorState.keySet()); + LinkedHashSet orderedHandlers; + try { + orderedHandlers = orderedStateHandlers(operatorState.keySet()); + } catch (Exception e) { + List errors = List.of(e.getMessage()); + recordErrorState(namespace, stateVersionMetadata.version(), errors, OperatorErrorMetadata.ErrorKind.PARSING); + logger.error("Error processing state change request for [{}] with the following errors [{}]", namespace, errors); + + throw new IllegalStateException("Error processing state change request for " + namespace, e); + } ClusterState state = clusterService.state(); OperatorMetadata existingMetadata = state.metadata().operatorState(namespace); diff --git a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/action/operator/OperatorILMControllerTests.java b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/action/operator/OperatorILMControllerTests.java index 2d83f828ed051..c903803bb36c7 100644 --- a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/action/operator/OperatorILMControllerTests.java +++ b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/action/operator/OperatorILMControllerTests.java @@ -129,7 +129,7 @@ public void testOperatorController() throws IOException { try (XContentParser parser = XContentType.JSON.xContent().createParser(XContentParserConfiguration.EMPTY, testJSON)) { assertEquals( - "Unknown settings definition type: ilm", + "Error processing state change request for operator", expectThrows(IllegalStateException.class, () -> controller.process("operator", parser)).getMessage() ); } From 3bd1468b98b9c86d238c0fe1a87c510a5520d0d7 Mon Sep 17 00:00:00 2001 From: Nikola Grcevski Date: Mon, 6 Jun 2022 14:31:49 -0400 Subject: [PATCH 26/48] Add verification for operator set keys --- .../TransportClusterUpdateSettingsAction.java | 13 ++++++ .../master/TransportMasterNodeAction.java | 42 +++++++++++++++++++ .../cluster/metadata/OperatorMetadata.java | 13 ++++++ .../OperatorClusterStateControllerTests.java | 11 ++++- 4 files changed, 78 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/settings/TransportClusterUpdateSettingsAction.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/settings/TransportClusterUpdateSettingsAction.java index 8858298de9876..dfaf552df311b 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/cluster/settings/TransportClusterUpdateSettingsAction.java +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/settings/TransportClusterUpdateSettingsAction.java @@ -29,11 +29,13 @@ import org.elasticsearch.common.settings.ClusterSettings; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.core.SuppressForbidden; +import org.elasticsearch.operator.action.OperatorClusterUpdateSettingsAction; import org.elasticsearch.tasks.Task; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.TransportService; import java.util.HashSet; +import java.util.Optional; import java.util.Set; import static org.elasticsearch.common.settings.AbstractScopedSettings.ARCHIVED_SETTINGS_PREFIX; @@ -128,6 +130,17 @@ private static boolean checkClearedBlockAndArchivedSettings( return true; } + @Override + protected Optional operatorHandlerName() { + return Optional.of(OperatorClusterUpdateSettingsAction.KEY); + } + + @Override + protected Set modifiedKeys(ClusterUpdateSettingsRequest request) { + Settings allSettings = Settings.builder().put(request.persistentSettings()).put(request.transientSettings()).build(); + return allSettings.keySet(); + } + private static final String UPDATE_TASK_SOURCE = "cluster_update_settings"; private static final String REROUTE_TASK_SOURCE = "reroute_after_cluster_update_settings"; diff --git a/server/src/main/java/org/elasticsearch/action/support/master/TransportMasterNodeAction.java b/server/src/main/java/org/elasticsearch/action/support/master/TransportMasterNodeAction.java index d92fb8f2956f9..080b3ff4fe302 100644 --- a/server/src/main/java/org/elasticsearch/action/support/master/TransportMasterNodeAction.java +++ b/server/src/main/java/org/elasticsearch/action/support/master/TransportMasterNodeAction.java @@ -23,6 +23,7 @@ import org.elasticsearch.cluster.block.ClusterBlockException; import org.elasticsearch.cluster.block.ClusterBlockLevel; import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; +import org.elasticsearch.cluster.metadata.OperatorMetadata; import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.cluster.node.DiscoveryNodes; import org.elasticsearch.cluster.service.ClusterService; @@ -42,6 +43,11 @@ import org.elasticsearch.transport.TransportException; import org.elasticsearch.transport.TransportService; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.Set; import java.util.function.Predicate; import static org.elasticsearch.core.Strings.format; @@ -142,9 +148,45 @@ private ClusterBlockException checkBlockIfStateRecovered(Request request, Cluste } } + protected Optional operatorHandlerName() { + return Optional.empty(); + } + + boolean supportsOperatorSetState() { + return operatorHandlerName().isPresent(); + } + + protected Set modifiedKeys(Request request) { + return Collections.emptySet(); + } + + void validateForOperatorState(Request request, ClusterState state) { + Optional handlerName = operatorHandlerName(); + assert handlerName.isPresent(); + + Set modified = modifiedKeys(request); + List errors = new ArrayList<>(); + + for (OperatorMetadata operator : state.metadata().operatorState().values()) { + Set conflicts = operator.conflicts(handlerName.get(), modified); + if (conflicts.isEmpty() == false) { + errors.add(format("[%s] set in operator mode by [%s]", String.join(",", conflicts), operator.namespace())); + } + } + + if (errors.isEmpty() == false) { + throw new IllegalStateException( + format("Failed to process request [%s] with errors: %s", request, String.join(System.lineSeparator(), errors)) + ); + } + } + @Override protected void doExecute(Task task, final Request request, ActionListener listener) { ClusterState state = clusterService.state(); + if (supportsOperatorSetState()) { + validateForOperatorState(request, state); + } logger.trace("starting processing request [{}] with cluster state version [{}]", request, state.version()); if (task != null) { request.setParentTask(clusterService.localNode().getId(), task.getId()); diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/OperatorMetadata.java b/server/src/main/java/org/elasticsearch/cluster/metadata/OperatorMetadata.java index a0c030f0acd14..481d8ab10863c 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/OperatorMetadata.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/OperatorMetadata.java @@ -21,8 +21,10 @@ import java.io.IOException; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; /** * Metadata class that contains information about cluster settings/entities set @@ -73,6 +75,17 @@ public Map handlers() { return handlers; } + public Set conflicts(String handlerName, Set modified) { + OperatorHandlerMetadata handlerMetadata = handlers.get(handlerName); + if (handlerMetadata == null || handlerMetadata.keys().isEmpty()) { + return Collections.emptySet(); + } + + Set intersect = new HashSet<>(handlerMetadata.keys()); + intersect.retainAll(modified); + return Collections.unmodifiableSet(intersect); + } + public static OperatorMetadata readFrom(StreamInput in) throws IOException { Builder builder = new Builder(in.readString()).version(in.readLong()); diff --git a/server/src/test/java/org/elasticsearch/operator/OperatorClusterStateControllerTests.java b/server/src/test/java/org/elasticsearch/operator/OperatorClusterStateControllerTests.java index e810089f49736..3e84d72b3bee4 100644 --- a/server/src/test/java/org/elasticsearch/operator/OperatorClusterStateControllerTests.java +++ b/server/src/test/java/org/elasticsearch/operator/OperatorClusterStateControllerTests.java @@ -68,7 +68,16 @@ public void testOperatorController() throws IOException { }, "state": { "cluster_settings": { - "indices.recovery.max_bytes_per_sec": "50mb" + "indices.recovery.max_bytes_per_sec": "50mb", + "cluster": { + "remote": { + "cluster_one": { + "seeds": [ + "127.0.0.1:9300" + ] + } + } + } } } } From 1f07274e2dc296ee0bd4535bb04d2b86ef8d0aed Mon Sep 17 00:00:00 2001 From: Nikola Grcevski Date: Mon, 6 Jun 2022 14:44:50 -0400 Subject: [PATCH 27/48] Add checking for ILM policies --- .../core/ilm/action/DeleteLifecycleAction.java | 7 +++++++ .../ilm/action/TransportDeleteLifecycleAction.java | 13 +++++++++++++ .../ilm/action/TransportPutLifecycleAction.java | 13 +++++++++++++ 3 files changed, 33 insertions(+) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/action/DeleteLifecycleAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/action/DeleteLifecycleAction.java index 623c9797ffde1..2d59d07a55d0d 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/action/DeleteLifecycleAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/action/DeleteLifecycleAction.java @@ -18,6 +18,8 @@ import java.io.IOException; import java.util.Objects; +import static org.elasticsearch.core.Strings.format; + public class DeleteLifecycleAction extends ActionType { public static final DeleteLifecycleAction INSTANCE = new DeleteLifecycleAction(); public static final String NAME = "cluster:admin/ilm/delete"; @@ -75,6 +77,11 @@ public boolean equals(Object obj) { return Objects.equals(policyName, other.policyName); } + @Override + public String toString() { + return format("delete lifecycle policy [%s]", policyName); + } + } } diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportDeleteLifecycleAction.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportDeleteLifecycleAction.java index 4480d6571a8c9..ba8ab6217e8b6 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportDeleteLifecycleAction.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportDeleteLifecycleAction.java @@ -29,8 +29,11 @@ import org.elasticsearch.xpack.core.ilm.LifecyclePolicyMetadata; import org.elasticsearch.xpack.core.ilm.action.DeleteLifecycleAction; import org.elasticsearch.xpack.core.ilm.action.DeleteLifecycleAction.Request; +import org.elasticsearch.xpack.ilm.action.operator.OperatorLifecycleAction; import java.util.List; +import java.util.Optional; +import java.util.Set; import java.util.SortedMap; import java.util.TreeMap; @@ -111,4 +114,14 @@ private void submitUnbatchedTask(@SuppressWarnings("SameParameterValue") String protected ClusterBlockException checkBlock(Request request, ClusterState state) { return state.blocks().globalBlockedException(ClusterBlockLevel.METADATA_WRITE); } + + @Override + protected Optional operatorHandlerName() { + return Optional.of(OperatorLifecycleAction.KEY); + } + + @Override + protected Set modifiedKeys(Request request) { + return Set.of(request.getPolicyName()); + } } diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportPutLifecycleAction.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportPutLifecycleAction.java index 05d501f33de7e..bc6df5c7696b5 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportPutLifecycleAction.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportPutLifecycleAction.java @@ -41,11 +41,14 @@ import org.elasticsearch.xpack.core.ilm.action.PutLifecycleAction; import org.elasticsearch.xpack.core.ilm.action.PutLifecycleAction.Request; import org.elasticsearch.xpack.core.slm.SnapshotLifecycleMetadata; +import org.elasticsearch.xpack.ilm.action.operator.OperatorLifecycleAction; import java.time.Instant; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; +import java.util.Set; import java.util.SortedMap; import java.util.TreeMap; @@ -295,4 +298,14 @@ private static void validatePrerequisites(LifecyclePolicy policy, ClusterState s protected ClusterBlockException checkBlock(Request request, ClusterState state) { return state.blocks().globalBlockedException(ClusterBlockLevel.METADATA_WRITE); } + + @Override + protected Optional operatorHandlerName() { + return Optional.of(OperatorLifecycleAction.KEY); + } + + @Override + protected Set modifiedKeys(Request request) { + return Set.of(request.getPolicy().getName()); + } } From 02b43a622839a39bb5116e15c46db57c51866331 Mon Sep 17 00:00:00 2001 From: Nikola Grcevski Date: Mon, 6 Jun 2022 16:43:04 -0400 Subject: [PATCH 28/48] Fix compile error after merge --- .../elasticsearch/operator/service/OperatorUpdateErrorTask.java | 2 +- .../elasticsearch/operator/service/OperatorUpdateStateTask.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/operator/service/OperatorUpdateErrorTask.java b/server/src/main/java/org/elasticsearch/operator/service/OperatorUpdateErrorTask.java index 1bfe1dcd53fc0..1297505892e38 100644 --- a/server/src/main/java/org/elasticsearch/operator/service/OperatorUpdateErrorTask.java +++ b/server/src/main/java/org/elasticsearch/operator/service/OperatorUpdateErrorTask.java @@ -54,7 +54,7 @@ public record OperatorUpdateErrorTaskExecutor( public ClusterState execute(ClusterState currentState, List> taskContexts) throws Exception { for (final var taskContext : taskContexts) { taskContext.success( - taskContext.getTask().listener().delegateFailure((l, s) -> l.onResponse(ActionResponse.Empty.INSTANCE)) + () -> taskContext.getTask().listener().delegateFailure((l, s) -> l.onResponse(ActionResponse.Empty.INSTANCE)) ); } diff --git a/server/src/main/java/org/elasticsearch/operator/service/OperatorUpdateStateTask.java b/server/src/main/java/org/elasticsearch/operator/service/OperatorUpdateStateTask.java index 07ce16e40aa84..49dcb1e3b6968 100644 --- a/server/src/main/java/org/elasticsearch/operator/service/OperatorUpdateStateTask.java +++ b/server/src/main/java/org/elasticsearch/operator/service/OperatorUpdateStateTask.java @@ -47,7 +47,7 @@ public record OperatorUpdateStateTaskExecutor(String namespace, ClusterState new public ClusterState execute(ClusterState currentState, List> taskContexts) throws Exception { for (final var taskContext : taskContexts) { taskContext.success( - taskContext.getTask().listener().delegateFailure((l, s) -> l.onResponse(ActionResponse.Empty.INSTANCE)) + () -> taskContext.getTask().listener().delegateFailure((l, s) -> l.onResponse(ActionResponse.Empty.INSTANCE)) ); } return newState; From 95d906b8d09689f6e848761fa4456713a1f5dce5 Mon Sep 17 00:00:00 2001 From: Nikola Grcevski Date: Tue, 7 Jun 2022 11:35:35 -0400 Subject: [PATCH 29/48] Add more ILM action tests --- .../operator/OperatorILMControllerTests.java | 140 ++++++++++++++++++ 1 file changed, 140 insertions(+) diff --git a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/action/operator/OperatorILMControllerTests.java b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/action/operator/OperatorILMControllerTests.java index c903803bb36c7..cc88dbfcd9932 100644 --- a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/action/operator/OperatorILMControllerTests.java +++ b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/action/operator/OperatorILMControllerTests.java @@ -15,11 +15,13 @@ import org.elasticsearch.common.settings.ClusterSettings; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.license.XPackLicenseState; +import org.elasticsearch.operator.TransformState; import org.elasticsearch.operator.action.OperatorClusterUpdateSettingsAction; import org.elasticsearch.operator.service.OperatorClusterStateController; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.xcontent.NamedXContentRegistry; import org.elasticsearch.xcontent.ParseField; +import org.elasticsearch.xcontent.XContentParseException; import org.elasticsearch.xcontent.XContentParser; import org.elasticsearch.xcontent.XContentParserConfiguration; import org.elasticsearch.xcontent.XContentType; @@ -27,6 +29,7 @@ import org.elasticsearch.xpack.core.ilm.DeleteAction; import org.elasticsearch.xpack.core.ilm.ForceMergeAction; import org.elasticsearch.xpack.core.ilm.FreezeAction; +import org.elasticsearch.xpack.core.ilm.IndexLifecycleMetadata; import org.elasticsearch.xpack.core.ilm.LifecycleAction; import org.elasticsearch.xpack.core.ilm.LifecycleType; import org.elasticsearch.xpack.core.ilm.MigrateAction; @@ -43,8 +46,10 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; +import static org.hamcrest.Matchers.containsInAnyOrder; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -85,6 +90,141 @@ protected NamedXContentRegistry xContentRegistry() { return new NamedXContentRegistry(entries); } + private TransformState processJSON(OperatorLifecycleAction action, TransformState prevState, String json) throws Exception { + try (XContentParser parser = XContentType.JSON.xContent().createParser(XContentParserConfiguration.EMPTY, json)) { + return action.transform(parser.map(), prevState); + } + } + + public void testValidationFails() { + Client client = mock(Client.class); + when(client.settings()).thenReturn(Settings.EMPTY); + final ClusterName clusterName = new ClusterName("elasticsearch"); + + ClusterState state = ClusterState.builder(clusterName).build(); + OperatorLifecycleAction action = new OperatorLifecycleAction(xContentRegistry(), client, mock(XPackLicenseState.class)); + TransformState prevState = new TransformState(state, Collections.emptySet()); + + String badPolicyJSON = """ + { + "my_timeseries_lifecycle": { + "polcy": { + "phases": { + "warm": { + "min_age": "10s", + "actions": { + } + } + } + } + } + }"""; + + assertEquals( + "[1:2] [put_lifecycle_request] unknown field [polcy] did you mean [policy]?", + expectThrows(XContentParseException.class, () -> processJSON(action, prevState, badPolicyJSON)).getMessage() + ); + } + + public void testActionAddRemove() throws Exception { + Client client = mock(Client.class); + when(client.settings()).thenReturn(Settings.EMPTY); + final ClusterName clusterName = new ClusterName("elasticsearch"); + + ClusterState state = ClusterState.builder(clusterName).build(); + + OperatorLifecycleAction action = new OperatorLifecycleAction(xContentRegistry(), client, mock(XPackLicenseState.class)); + + String emptyJSON = ""; + + TransformState prevState = new TransformState(state, Collections.emptySet()); + + TransformState updatedState = processJSON(action, prevState, emptyJSON); + assertEquals(0, updatedState.keys().size()); + assertEquals(prevState.state(), updatedState.state()); + + String twoPoliciesJSON = """ + { + "my_timeseries_lifecycle": { + "policy": { + "phases": { + "warm": { + "min_age": "10s", + "actions": { + } + } + } + } + }, + "my_timeseries_lifecycle1": { + "policy": { + "phases": { + "warm": { + "min_age": "10s", + "actions": { + } + }, + "delete": { + "min_age": "30s", + "actions": { + } + } + } + } + } + }"""; + + prevState = updatedState; + updatedState = processJSON(action, prevState, twoPoliciesJSON); + assertThat(updatedState.keys(), containsInAnyOrder("my_timeseries_lifecycle", "my_timeseries_lifecycle1")); + IndexLifecycleMetadata ilmMetadata = updatedState.state() + .metadata() + .custom(IndexLifecycleMetadata.TYPE, IndexLifecycleMetadata.EMPTY); + assertThat(ilmMetadata.getPolicyMetadatas().keySet(), containsInAnyOrder("my_timeseries_lifecycle", "my_timeseries_lifecycle1")); + + String onePolicyRemovedJSON = """ + { + "my_timeseries_lifecycle": { + "policy": { + "phases": { + "warm": { + "min_age": "10s", + "actions": { + } + } + } + } + } + }"""; + + prevState = updatedState; + updatedState = processJSON(action, prevState, onePolicyRemovedJSON); + assertThat(updatedState.keys(), containsInAnyOrder("my_timeseries_lifecycle")); + ilmMetadata = updatedState.state().metadata().custom(IndexLifecycleMetadata.TYPE, IndexLifecycleMetadata.EMPTY); + assertThat(ilmMetadata.getPolicyMetadatas().keySet(), containsInAnyOrder("my_timeseries_lifecycle")); + + String onePolicyRenamedJSON = """ + { + "my_timeseries_lifecycle2": { + "policy": { + "phases": { + "warm": { + "min_age": "10s", + "actions": { + } + } + } + } + } + }"""; + + prevState = updatedState; + updatedState = processJSON(action, prevState, onePolicyRenamedJSON); + assertThat(updatedState.keys(), containsInAnyOrder("my_timeseries_lifecycle2")); + ilmMetadata = updatedState.state().metadata().custom(IndexLifecycleMetadata.TYPE, IndexLifecycleMetadata.EMPTY); + assertThat(ilmMetadata.getPolicyMetadatas().keySet(), containsInAnyOrder("my_timeseries_lifecycle2")); + } + public void testOperatorController() throws IOException { ClusterSettings clusterSettings = new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS); ClusterService clusterService = mock(ClusterService.class); From 931db49accb4a852e9806f3639955b638ee83f2e Mon Sep 17 00:00:00 2001 From: Nikola Grcevski Date: Tue, 7 Jun 2022 11:55:24 -0400 Subject: [PATCH 30/48] Add ilm transport operator tests --- .../TransportDeleteLifecycleActionTests.java | 36 ++++++++++++++ .../TransportPutLifecycleActionTests.java | 49 +++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/action/TransportDeleteLifecycleActionTests.java diff --git a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/action/TransportDeleteLifecycleActionTests.java b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/action/TransportDeleteLifecycleActionTests.java new file mode 100644 index 0000000000000..2f46e1bafc660 --- /dev/null +++ b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/action/TransportDeleteLifecycleActionTests.java @@ -0,0 +1,36 @@ +/* + * 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.ilm.action; + +import org.elasticsearch.action.support.ActionFilters; +import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; +import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.transport.TransportService; +import org.elasticsearch.xpack.core.ilm.action.DeleteLifecycleAction; +import org.elasticsearch.xpack.ilm.action.operator.OperatorLifecycleAction; + +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.mockito.Mockito.mock; + +public class TransportDeleteLifecycleActionTests extends ESTestCase { + public void testOperatorHandler() throws Exception { + TransportDeleteLifecycleAction putAction = new TransportDeleteLifecycleAction( + mock(TransportService.class), + mock(ClusterService.class), + mock(ThreadPool.class), + mock(ActionFilters.class), + mock(IndexNameExpressionResolver.class) + ); + assertEquals(OperatorLifecycleAction.KEY, putAction.operatorHandlerName().get()); + + DeleteLifecycleAction.Request request = new DeleteLifecycleAction.Request("my_timeseries_lifecycle2"); + assertThat(putAction.modifiedKeys(request), containsInAnyOrder("my_timeseries_lifecycle2")); + } +} diff --git a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/action/TransportPutLifecycleActionTests.java b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/action/TransportPutLifecycleActionTests.java index b2202f0e5126f..a14a6cd5f7623 100644 --- a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/action/TransportPutLifecycleActionTests.java +++ b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/action/TransportPutLifecycleActionTests.java @@ -7,13 +7,29 @@ package org.elasticsearch.xpack.ilm.action; +import org.elasticsearch.action.support.ActionFilters; +import org.elasticsearch.client.internal.Client; +import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; +import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.transport.TransportService; +import org.elasticsearch.xcontent.NamedXContentRegistry; +import org.elasticsearch.xcontent.XContentParser; +import org.elasticsearch.xcontent.XContentParserConfiguration; +import org.elasticsearch.xcontent.XContentType; import org.elasticsearch.xpack.core.ilm.LifecyclePolicy; import org.elasticsearch.xpack.core.ilm.LifecyclePolicyMetadata; +import org.elasticsearch.xpack.core.ilm.action.PutLifecycleAction; import org.elasticsearch.xpack.ilm.LifecyclePolicyTestsUtils; +import org.elasticsearch.xpack.ilm.action.operator.OperatorLifecycleAction; import java.util.Map; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.mockito.Mockito.mock; + public class TransportPutLifecycleActionTests extends ESTestCase { public void testIsNoop() { LifecyclePolicy policy1 = LifecyclePolicyTestsUtils.randomTimeseriesLifecyclePolicy("policy"); @@ -29,4 +45,37 @@ public void testIsNoop() { assertFalse(TransportPutLifecycleAction.isNoopUpdate(existing, policy1, headers2)); assertFalse(TransportPutLifecycleAction.isNoopUpdate(null, policy1, headers1)); } + + public void testOperatorHandler() throws Exception { + TransportPutLifecycleAction putAction = new TransportPutLifecycleAction( + mock(TransportService.class), + mock(ClusterService.class), + mock(ThreadPool.class), + mock(ActionFilters.class), + mock(IndexNameExpressionResolver.class), + mock(NamedXContentRegistry.class), + mock(XPackLicenseState.class), + mock(Client.class) + ); + assertEquals(OperatorLifecycleAction.KEY, putAction.operatorHandlerName().get()); + + String json = """ + { + "policy": { + "phases": { + "warm": { + "min_age": "10s", + "actions": { + } + } + } + } + }"""; + + try (XContentParser parser = XContentType.JSON.xContent().createParser(XContentParserConfiguration.EMPTY, json)) { + PutLifecycleAction.Request request = PutLifecycleAction.Request.parseRequest("my_timeseries_lifecycle2", parser); + + assertThat(putAction.modifiedKeys(request), containsInAnyOrder("my_timeseries_lifecycle2")); + } + } } From aac849261ce040c14d5238a1be3d78b578cd690c Mon Sep 17 00:00:00 2001 From: Nikola Grcevski Date: Tue, 7 Jun 2022 11:57:06 -0400 Subject: [PATCH 31/48] Revert not needed change --- .../xpack/ilm/action/RestPutLifecycleAction.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/RestPutLifecycleAction.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/RestPutLifecycleAction.java index 06a3fd5223c16..f15a244710987 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/RestPutLifecycleAction.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/RestPutLifecycleAction.java @@ -21,8 +21,6 @@ public class RestPutLifecycleAction extends BaseRestHandler { - public static final String NAME = "name"; - @Override public List routes() { return List.of(new Route(PUT, "/_ilm/policy/{name}")); @@ -35,7 +33,7 @@ public String getName() { @Override protected RestChannelConsumer prepareRequest(RestRequest restRequest, NodeClient client) throws IOException { - String lifecycleName = restRequest.param(NAME); + String lifecycleName = restRequest.param("name"); try (XContentParser parser = restRequest.contentParser()) { PutLifecycleAction.Request putLifecycleRequest = PutLifecycleAction.Request.parseRequest(lifecycleName, parser); putLifecycleRequest.timeout(restRequest.paramAsTime("timeout", putLifecycleRequest.timeout())); From 936376e0bc6f87abcf830bd4cb2c0ca943e0aed5 Mon Sep 17 00:00:00 2001 From: Nikola Grcevski Date: Tue, 7 Jun 2022 14:04:24 -0400 Subject: [PATCH 32/48] Add more unit tests --- .../OperatorClusterUpdateSettingsAction.java | 6 +- .../ClusterUpdateSettingsRequestTests.java | 49 +++++ .../metadata/ToAndFromJsonMetadataTests.java | 175 ++++++++++++++++++ ...ratorClusterUpdateSettingsActionTests.java | 100 ++++++++++ .../TransportDeleteLifecycleActionTests.java | 2 +- 5 files changed, 330 insertions(+), 2 deletions(-) create mode 100644 server/src/test/java/org/elasticsearch/operator/action/OperatorClusterUpdateSettingsActionTests.java diff --git a/server/src/main/java/org/elasticsearch/operator/action/OperatorClusterUpdateSettingsAction.java b/server/src/main/java/org/elasticsearch/operator/action/OperatorClusterUpdateSettingsAction.java index 8f517d9224317..94777d2df86e2 100644 --- a/server/src/main/java/org/elasticsearch/operator/action/OperatorClusterUpdateSettingsAction.java +++ b/server/src/main/java/org/elasticsearch/operator/action/OperatorClusterUpdateSettingsAction.java @@ -62,7 +62,11 @@ private ClusterUpdateSettingsRequest prepare(Object input, Set previousl @Override public TransformState transform(Object input, TransformState prevState) { ClusterUpdateSettingsRequest request = prepare(input, prevState.keys()); - validate(request); + + // allow empty requests, this is how we clean up settings + if (request.persistentSettings().isEmpty() == false) { + validate(request); + } ClusterState state = prevState.state(); diff --git a/server/src/test/java/org/elasticsearch/action/admin/cluster/settings/ClusterUpdateSettingsRequestTests.java b/server/src/test/java/org/elasticsearch/action/admin/cluster/settings/ClusterUpdateSettingsRequestTests.java index 042f7f150788a..4fd92c1c32268 100644 --- a/server/src/test/java/org/elasticsearch/action/admin/cluster/settings/ClusterUpdateSettingsRequestTests.java +++ b/server/src/test/java/org/elasticsearch/action/admin/cluster/settings/ClusterUpdateSettingsRequestTests.java @@ -8,9 +8,17 @@ package org.elasticsearch.action.admin.cluster.settings; +import org.elasticsearch.action.support.ActionFilters; +import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; +import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.settings.ClusterSettings; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.operator.action.OperatorClusterUpdateSettingsAction; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.XContentTestUtils; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.transport.TransportService; import org.elasticsearch.xcontent.ToXContent; import org.elasticsearch.xcontent.XContentParseException; import org.elasticsearch.xcontent.XContentParser; @@ -21,6 +29,8 @@ import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.mockito.Mockito.mock; public class ClusterUpdateSettingsRequestTests extends ESTestCase { @@ -71,4 +81,43 @@ private static ClusterUpdateSettingsRequest createTestItem() { request.transientSettings(ClusterUpdateSettingsResponseTests.randomClusterSettings(0, 2)); return request; } + + public void testOperatorHandler() throws IOException { + ClusterSettings clusterSettings = new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS); + + TransportClusterUpdateSettingsAction action = new TransportClusterUpdateSettingsAction( + mock(TransportService.class), + mock(ClusterService.class), + mock(ThreadPool.class), + mock(ActionFilters.class), + mock(IndexNameExpressionResolver.class), + clusterSettings + ); + + assertEquals(OperatorClusterUpdateSettingsAction.KEY, action.operatorHandlerName().get()); + + String oneSettingJSON = """ + { + "persistent": { + "indices.recovery.max_bytes_per_sec": "25mb", + "cluster": { + "remote": { + "cluster_one": { + "seeds": [ + "127.0.0.1:9300" + ] + } + } + } + } + }"""; + + try (XContentParser parser = createParser(XContentType.JSON.xContent(), oneSettingJSON)) { + ClusterUpdateSettingsRequest parsedRequest = ClusterUpdateSettingsRequest.fromXContent(parser); + assertThat( + action.modifiedKeys(parsedRequest), + containsInAnyOrder("indices.recovery.max_bytes_per_sec", "cluster.remote.cluster_one.seeds") + ); + } + } } diff --git a/server/src/test/java/org/elasticsearch/cluster/metadata/ToAndFromJsonMetadataTests.java b/server/src/test/java/org/elasticsearch/cluster/metadata/ToAndFromJsonMetadataTests.java index 3ab133525ccb1..2f7500be70a75 100644 --- a/server/src/test/java/org/elasticsearch/cluster/metadata/ToAndFromJsonMetadataTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/metadata/ToAndFromJsonMetadataTests.java @@ -29,6 +29,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import static org.elasticsearch.cluster.metadata.AliasMetadata.newAliasMetadataBuilder; import static org.elasticsearch.cluster.metadata.DataStreamTestHelper.createFirstBackingIndex; @@ -625,6 +626,180 @@ public void testToXContentAPI_FlatSettingFalse_ReduceMappingTrue() throws IOExce }""".formatted(Version.CURRENT.id, Version.CURRENT.id), Strings.toString(builder)); } + public void testToXContentAPIOperatorMetadata() throws IOException { + Map mapParams = new HashMap<>() { + { + put(Metadata.CONTEXT_MODE_PARAM, CONTEXT_MODE_API); + put("flat_settings", "false"); + put("reduce_mappings", "true"); + } + }; + + Metadata metadata = buildMetadata(); + + OperatorHandlerMetadata hmOne = new OperatorHandlerMetadata.Builder("one").keys(Set.of("a", "b")).build(); + OperatorHandlerMetadata hmTwo = new OperatorHandlerMetadata.Builder("two").keys(Set.of("c", "d")).build(); + OperatorHandlerMetadata hmThree = new OperatorHandlerMetadata.Builder("three").keys(Set.of("e", "f")).build(); + + OperatorErrorMetadata emOne = new OperatorErrorMetadata.Builder().version(1L) + .errorKind(OperatorErrorMetadata.ErrorKind.VALIDATION) + .errors(List.of("Test error 1", "Test error 2")) + .build(); + + OperatorErrorMetadata emTwo = new OperatorErrorMetadata.Builder().version(2L) + .errorKind(OperatorErrorMetadata.ErrorKind.TRANSIENT) + .errors(List.of("Test error 3", "Test error 4")) + .build(); + + OperatorMetadata omOne = OperatorMetadata.builder("namespace_one").errorMetadata(emOne).putHandler(hmOne).putHandler(hmTwo).build(); + + OperatorMetadata omTwo = OperatorMetadata.builder("namespace_two").errorMetadata(emTwo).putHandler(hmThree).build(); + + metadata = Metadata.builder(metadata).putOperatorState(omOne).putOperatorState(omTwo).build(); + + XContentBuilder builder = JsonXContent.contentBuilder().prettyPrint(); + builder.startObject(); + metadata.toXContent(builder, new ToXContent.MapParams(mapParams)); + builder.endObject(); + + assertEquals(""" + { + "metadata" : { + "cluster_uuid" : "clusterUUID", + "cluster_uuid_committed" : false, + "cluster_coordination" : { + "term" : 1, + "last_committed_config" : [ + "commitedConfigurationNodeId" + ], + "last_accepted_config" : [ + "acceptedConfigurationNodeId" + ], + "voting_config_exclusions" : [ + { + "node_id" : "exlucdedNodeId", + "node_name" : "excludedNodeName" + } + ] + }, + "templates" : { + "template" : { + "order" : 0, + "index_patterns" : [ + "pattern1", + "pattern2" + ], + "settings" : { + "index" : { + "version" : { + "created" : "%s" + } + } + }, + "mappings" : { }, + "aliases" : { } + } + }, + "indices" : { + "index" : { + "version" : 2, + "mapping_version" : 1, + "settings_version" : 1, + "aliases_version" : 1, + "routing_num_shards" : 1, + "state" : "open", + "settings" : { + "index" : { + "number_of_shards" : "1", + "number_of_replicas" : "2", + "version" : { + "created" : "%s" + } + } + }, + "mappings" : { + "type" : { + "type1" : { + "key" : "value" + } + } + }, + "aliases" : [ + "alias" + ], + "primary_terms" : { + "0" : 1 + }, + "in_sync_allocations" : { + "0" : [ + "allocationId" + ] + }, + "rollover_info" : { + "rolloveAlias" : { + "met_conditions" : { }, + "time" : 1 + } + }, + "system" : false, + "timestamp_range" : { + "shards" : [ ] + } + } + }, + "index-graveyard" : { + "tombstones" : [ ] + }, + "operator" : { + "namespace_one" : { + "version" : 0, + "handlers" : { + "one" : { + "keys" : [ + "b", + "a" + ] + }, + "two" : { + "keys" : [ + "d", + "c" + ] + } + }, + "errors" : { + "version" : 1, + "error_kind" : "validation", + "errors" : [ + "Test error 1", + "Test error 2" + ] + } + }, + "namespace_two" : { + "version" : 0, + "handlers" : { + "three" : { + "keys" : [ + "f", + "e" + ] + } + }, + "errors" : { + "version" : 2, + "error_kind" : "transient", + "errors" : [ + "Test error 3", + "Test error 4" + ] + } + } + } + } + }""".formatted(Version.CURRENT.id, Version.CURRENT.id), Strings.toString(builder)); + } + private Metadata buildMetadata() throws IOException { return Metadata.builder() .clusterUUID("clusterUUID") diff --git a/server/src/test/java/org/elasticsearch/operator/action/OperatorClusterUpdateSettingsActionTests.java b/server/src/test/java/org/elasticsearch/operator/action/OperatorClusterUpdateSettingsActionTests.java new file mode 100644 index 0000000000000..c2e29a10117fa --- /dev/null +++ b/server/src/test/java/org/elasticsearch/operator/action/OperatorClusterUpdateSettingsActionTests.java @@ -0,0 +1,100 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.operator.action; + +import org.elasticsearch.cluster.ClusterName; +import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.common.settings.ClusterSettings; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.operator.TransformState; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xcontent.XContentParser; +import org.elasticsearch.xcontent.XContentParserConfiguration; +import org.elasticsearch.xcontent.XContentType; + +import java.util.Collections; + +import static org.hamcrest.Matchers.containsInAnyOrder; + +public class OperatorClusterUpdateSettingsActionTests extends ESTestCase { + + private TransformState processJSON(OperatorClusterUpdateSettingsAction action, TransformState prevState, String json) throws Exception { + try (XContentParser parser = XContentType.JSON.xContent().createParser(XContentParserConfiguration.EMPTY, json)) { + return action.transform(parser.map(), prevState); + } + } + + public void testValidation() throws Exception { + ClusterSettings clusterSettings = new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS); + + ClusterState state = ClusterState.builder(new ClusterName("elasticsearch")).build(); + TransformState prevState = new TransformState(state, Collections.emptySet()); + OperatorClusterUpdateSettingsAction action = new OperatorClusterUpdateSettingsAction(clusterSettings); + + String badPolicyJSON = """ + { + "indices.recovery.min_bytes_per_sec": "50mb" + }"""; + + assertEquals( + "persistent setting [indices.recovery.min_bytes_per_sec], not recognized", + expectThrows(IllegalArgumentException.class, () -> processJSON(action, prevState, badPolicyJSON)).getMessage() + ); + } + + public void testSetUnsetSettings() throws Exception { + ClusterSettings clusterSettings = new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS); + + ClusterState state = ClusterState.builder(new ClusterName("elasticsearch")).build(); + TransformState prevState = new TransformState(state, Collections.emptySet()); + OperatorClusterUpdateSettingsAction action = new OperatorClusterUpdateSettingsAction(clusterSettings); + + String emptyJSON = ""; + + TransformState updatedState = processJSON(action, prevState, emptyJSON); + assertEquals(0, updatedState.keys().size()); + assertEquals(prevState.state(), updatedState.state()); + + String settingsJSON = """ + { + "indices.recovery.max_bytes_per_sec": "50mb", + "cluster": { + "remote": { + "cluster_one": { + "seeds": [ + "127.0.0.1:9300" + ] + } + } + } + }"""; + + prevState = updatedState; + updatedState = processJSON(action, prevState, settingsJSON); + assertThat(updatedState.keys(), containsInAnyOrder("indices.recovery.max_bytes_per_sec", "cluster.remote.cluster_one.seeds")); + assertEquals("50mb", updatedState.state().metadata().persistentSettings().get("indices.recovery.max_bytes_per_sec")); + assertEquals("[127.0.0.1:9300]", updatedState.state().metadata().persistentSettings().get("cluster.remote.cluster_one.seeds")); + + String oneSettingJSON = """ + { + "indices.recovery.max_bytes_per_sec": "25mb" + }"""; + + prevState = updatedState; + updatedState = processJSON(action, prevState, oneSettingJSON); + assertThat(updatedState.keys(), containsInAnyOrder("indices.recovery.max_bytes_per_sec")); + assertEquals("25mb", updatedState.state().metadata().persistentSettings().get("indices.recovery.max_bytes_per_sec")); + assertNull(updatedState.state().metadata().persistentSettings().get("cluster.remote.cluster_one.seeds")); + + prevState = updatedState; + updatedState = processJSON(action, prevState, emptyJSON); + assertEquals(0, updatedState.keys().size()); + assertNull(updatedState.state().metadata().persistentSettings().get("indices.recovery.max_bytes_per_sec")); + } +} diff --git a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/action/TransportDeleteLifecycleActionTests.java b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/action/TransportDeleteLifecycleActionTests.java index 2f46e1bafc660..fb583b1510350 100644 --- a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/action/TransportDeleteLifecycleActionTests.java +++ b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/action/TransportDeleteLifecycleActionTests.java @@ -20,7 +20,7 @@ import static org.mockito.Mockito.mock; public class TransportDeleteLifecycleActionTests extends ESTestCase { - public void testOperatorHandler() throws Exception { + public void testOperatorHandler() { TransportDeleteLifecycleAction putAction = new TransportDeleteLifecycleAction( mock(TransportService.class), mock(ClusterService.class), From 092af3ee9000ffc880c88b342fba1e98ec3b40fd Mon Sep 17 00:00:00 2001 From: Nikola Grcevski Date: Tue, 7 Jun 2022 16:16:09 -0400 Subject: [PATCH 33/48] Add controller tasks tests --- .../OperatorClusterStateController.java | 6 +- .../OperatorClusterStateControllerTests.java | 90 -------- .../OperatorClusterStateControllerTests.java | 200 ++++++++++++++++++ 3 files changed, 203 insertions(+), 93 deletions(-) delete mode 100644 server/src/test/java/org/elasticsearch/operator/OperatorClusterStateControllerTests.java create mode 100644 server/src/test/java/org/elasticsearch/operator/service/OperatorClusterStateControllerTests.java diff --git a/server/src/main/java/org/elasticsearch/operator/service/OperatorClusterStateController.java b/server/src/main/java/org/elasticsearch/operator/service/OperatorClusterStateController.java index 3e4f41a72ca25..9f90b9fd29bf3 100644 --- a/server/src/main/java/org/elasticsearch/operator/service/OperatorClusterStateController.java +++ b/server/src/main/java/org/elasticsearch/operator/service/OperatorClusterStateController.java @@ -152,8 +152,8 @@ public ClusterState process(String namespace, XContentParser parser) { Metadata.Builder metadataBuilder = Metadata.builder(state.metadata()).putOperatorState(operatorMetadataBuilder.build()); state = stateBuilder.metadata(metadataBuilder).build(); - // TODO: Retry, maybe use RetryableAction? - clusterService.submitStateUpdateTask("operator state [ " + namespace + "]", new OperatorUpdateStateTask(new ActionListener<>() { + // Do we need to retry this? + clusterService.submitStateUpdateTask("operator state [" + namespace + "]", new OperatorUpdateStateTask(new ActionListener<>() { @Override public void onResponse(ActionResponse.Empty empty) { logger.info("Successfully applied new cluster state for namespace [{}]", namespace); @@ -185,7 +185,7 @@ private Set keysForHandler(OperatorMetadata operatorMetadata, String han return operatorMetadata.handlers().get(handlerKey).keys(); } - boolean checkMetadataVersion(OperatorMetadata existingMetadata, OperatorStateVersionMetadata stateVersionMetadata) { + static boolean checkMetadataVersion(OperatorMetadata existingMetadata, OperatorStateVersionMetadata stateVersionMetadata) { if (Version.CURRENT.before(stateVersionMetadata.minCompatibleVersion())) { logger.info( "Cluster state version [{}] is not compatible with this Elasticsearch node", diff --git a/server/src/test/java/org/elasticsearch/operator/OperatorClusterStateControllerTests.java b/server/src/test/java/org/elasticsearch/operator/OperatorClusterStateControllerTests.java deleted file mode 100644 index 3e84d72b3bee4..0000000000000 --- a/server/src/test/java/org/elasticsearch/operator/OperatorClusterStateControllerTests.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * 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 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 or the Server - * Side Public License, v 1. - */ - -package org.elasticsearch.operator; - -import org.elasticsearch.cluster.ClusterName; -import org.elasticsearch.cluster.ClusterState; -import org.elasticsearch.cluster.service.ClusterService; -import org.elasticsearch.common.settings.ClusterSettings; -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.operator.action.OperatorClusterUpdateSettingsAction; -import org.elasticsearch.operator.service.OperatorClusterStateController; -import org.elasticsearch.test.ESTestCase; -import org.elasticsearch.xcontent.XContentParser; -import org.elasticsearch.xcontent.XContentParserConfiguration; -import org.elasticsearch.xcontent.XContentType; - -import java.io.IOException; -import java.util.List; - -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -public class OperatorClusterStateControllerTests extends ESTestCase { - - public void testOperatorController() throws IOException { - ClusterSettings clusterSettings = new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS); - ClusterService clusterService = mock(ClusterService.class); - final ClusterName clusterName = new ClusterName("elasticsearch"); - - ClusterState state = ClusterState.builder(clusterName).build(); - when(clusterService.state()).thenReturn(state); - - OperatorClusterStateController controller = new OperatorClusterStateController(clusterService); - controller.initHandlers(List.of(new OperatorClusterUpdateSettingsAction(clusterSettings))); - - String testJSON = """ - { - "metadata": { - "version": "1234", - "compatibility": "8.4.0" - }, - "state": { - "cluster_settings": { - "indices.recovery.max_bytes_per_sec": "50mb" - - } - } - """; - - try (XContentParser parser = XContentType.JSON.xContent().createParser(XContentParserConfiguration.EMPTY, testJSON)) { - assertEquals( - "Error processing state change request for operator", - expectThrows(IllegalStateException.class, () -> controller.process("operator", parser)).getMessage() - ); - } - - testJSON = """ - { - "metadata": { - "version": "1234", - "compatibility": "8.4.0" - }, - "state": { - "cluster_settings": { - "indices.recovery.max_bytes_per_sec": "50mb", - "cluster": { - "remote": { - "cluster_one": { - "seeds": [ - "127.0.0.1:9300" - ] - } - } - } - } - } - } - """; - - try (XContentParser parser = XContentType.JSON.xContent().createParser(XContentParserConfiguration.EMPTY, testJSON)) { - controller.process("operator", parser); - } - } -} diff --git a/server/src/test/java/org/elasticsearch/operator/service/OperatorClusterStateControllerTests.java b/server/src/test/java/org/elasticsearch/operator/service/OperatorClusterStateControllerTests.java new file mode 100644 index 0000000000000..1ed94cc621c7e --- /dev/null +++ b/server/src/test/java/org/elasticsearch/operator/service/OperatorClusterStateControllerTests.java @@ -0,0 +1,200 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.operator.service; + +import org.elasticsearch.Version; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.ActionResponse; +import org.elasticsearch.cluster.ClusterName; +import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.ClusterStateAckListener; +import org.elasticsearch.cluster.ClusterStateTaskExecutor; +import org.elasticsearch.cluster.metadata.OperatorErrorMetadata; +import org.elasticsearch.cluster.metadata.OperatorMetadata; +import org.elasticsearch.cluster.routing.RerouteService; +import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.common.settings.ClusterSettings; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.operator.action.OperatorClusterUpdateSettingsAction; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xcontent.XContentParser; +import org.elasticsearch.xcontent.XContentParserConfiguration; +import org.elasticsearch.xcontent.XContentType; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; + +import static org.hamcrest.Matchers.contains; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class OperatorClusterStateControllerTests extends ESTestCase { + + public void testOperatorController() throws IOException { + ClusterSettings clusterSettings = new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS); + ClusterService clusterService = mock(ClusterService.class); + final ClusterName clusterName = new ClusterName("elasticsearch"); + + ClusterState state = ClusterState.builder(clusterName).build(); + when(clusterService.state()).thenReturn(state); + + OperatorClusterStateController controller = new OperatorClusterStateController(clusterService); + controller.initHandlers(List.of(new OperatorClusterUpdateSettingsAction(clusterSettings))); + + String testJSON = """ + { + "metadata": { + "version": "1234", + "compatibility": "8.4.0" + }, + "state": { + "cluster_settings": { + "indices.recovery.max_bytes_per_sec": "50mb" + + } + } + """; + + try (XContentParser parser = XContentType.JSON.xContent().createParser(XContentParserConfiguration.EMPTY, testJSON)) { + assertEquals( + "Error processing state change request for operator", + expectThrows(IllegalStateException.class, () -> controller.process("operator", parser)).getMessage() + ); + } + + testJSON = """ + { + "metadata": { + "version": "1234", + "compatibility": "8.4.0" + }, + "state": { + "cluster_settings": { + "indices.recovery.max_bytes_per_sec": "50mb", + "cluster": { + "remote": { + "cluster_one": { + "seeds": [ + "127.0.0.1:9300" + ] + } + } + } + } + } + } + """; + + try (XContentParser parser = XContentType.JSON.xContent().createParser(XContentParserConfiguration.EMPTY, testJSON)) { + controller.process("operator", parser); + } + } + + public void testUpdateStateTasks() throws Exception { + ClusterService clusterService = mock(ClusterService.class); + RerouteService rerouteService = mock(RerouteService.class); + + when(clusterService.getRerouteService()).thenReturn(rerouteService); + ClusterState state = ClusterState.builder(new ClusterName("test")).build(); + + OperatorUpdateStateTask.OperatorUpdateStateTaskExecutor taskExecutor = new OperatorUpdateStateTask.OperatorUpdateStateTaskExecutor( + "test", + state, + clusterService.getRerouteService() + ); + + AtomicBoolean successCalled = new AtomicBoolean(false); + + OperatorUpdateStateTask task = new OperatorUpdateStateTask(new ActionListener<>() { + @Override + public void onResponse(ActionResponse.Empty empty) {} + + @Override + public void onFailure(Exception e) {} + }); + + ClusterStateTaskExecutor.TaskContext taskContext = new ClusterStateTaskExecutor.TaskContext<>() { + @Override + public OperatorUpdateStateTask getTask() { + return task; + } + + @Override + public void success(Runnable onPublicationSuccess) { + onPublicationSuccess.run(); + successCalled.set(true); + } + + @Override + public void success(Consumer publishedStateConsumer) {} + + @Override + public void success(Runnable onPublicationSuccess, ClusterStateAckListener clusterStateAckListener) {} + + @Override + public void success(Consumer publishedStateConsumer, ClusterStateAckListener clusterStateAckListener) {} + + @Override + public void onFailure(Exception failure) {} + }; + + ClusterState newState = taskExecutor.execute(state, List.of(taskContext)); + assertEquals(state, newState); + assertTrue(successCalled.get()); + + taskExecutor.clusterStatePublished(state); + verify(rerouteService, times(1)).reroute(anyString(), any(), any()); + } + + public void testErrorStateTask() throws Exception { + ClusterState state = ClusterState.builder(new ClusterName("test")).build(); + + OperatorUpdateErrorTask.OperatorUpdateErrorTaskExecutor executor = new OperatorUpdateErrorTask.OperatorUpdateErrorTaskExecutor( + "test", + 1L, + OperatorErrorMetadata.ErrorKind.PARSING, + List.of("some parse error", "some io error") + ); + + ClusterState newState = executor.execute(state, Collections.emptyList()); + + OperatorMetadata operatorMetadata = newState.metadata().operatorState("test"); + assertNotNull(operatorMetadata); + assertNotNull(operatorMetadata.errorMetadata()); + assertEquals(1L, (long) operatorMetadata.errorMetadata().version()); + assertEquals(OperatorErrorMetadata.ErrorKind.PARSING, operatorMetadata.errorMetadata().errorKind()); + assertThat(operatorMetadata.errorMetadata().errors(), contains("some parse error", "some io error")); + } + + public void testcheckMetadataVersion() { + OperatorMetadata operatorMetadata = OperatorMetadata.builder("test").version(123L).build(); + + assertTrue( + OperatorClusterStateController.checkMetadataVersion(operatorMetadata, new OperatorStateVersionMetadata(124L, Version.CURRENT)) + ); + + assertFalse( + OperatorClusterStateController.checkMetadataVersion(operatorMetadata, new OperatorStateVersionMetadata(123L, Version.CURRENT)) + ); + + assertFalse( + OperatorClusterStateController.checkMetadataVersion( + operatorMetadata, + new OperatorStateVersionMetadata(124L, Version.fromId(Version.CURRENT.id + 1)) + ) + ); + } +} From 002c5edf44542d745157249a002d5e71f02cb060 Mon Sep 17 00:00:00 2001 From: Nikola Grcevski Date: Tue, 7 Jun 2022 16:36:21 -0400 Subject: [PATCH 34/48] Add handler ordering unit tests --- .../OperatorClusterStateController.java | 13 ++- .../OperatorClusterStateControllerTests.java | 101 ++++++++++++++++++ 2 files changed, 111 insertions(+), 3 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/operator/service/OperatorClusterStateController.java b/server/src/main/java/org/elasticsearch/operator/service/OperatorClusterStateController.java index 9f90b9fd29bf3..d7f3a18e2eacc 100644 --- a/server/src/main/java/org/elasticsearch/operator/service/OperatorClusterStateController.java +++ b/server/src/main/java/org/elasticsearch/operator/service/OperatorClusterStateController.java @@ -56,11 +56,16 @@ public OperatorClusterStateController(ClusterService clusterService) { this.clusterService = clusterService; } + /** + * Initializes the controller with the currently implemented state handlers + * + * @param handlerList the list of supported operator handlers + */ public void initHandlers(List> handlerList) { handlers = handlerList.stream().collect(Collectors.toMap(OperatorHandler::key, Function.identity())); } - static class SettingsFile { + private static class SettingsFile { public static final ParseField STATE_FIELD = new ParseField("state"); public static final ParseField METADATA_FIELD = new ParseField("metadata"); @SuppressWarnings("unchecked") @@ -185,6 +190,7 @@ private Set keysForHandler(OperatorMetadata operatorMetadata, String han return operatorMetadata.handlers().get(handlerKey).keys(); } + // package private for testing static boolean checkMetadataVersion(OperatorMetadata existingMetadata, OperatorStateVersionMetadata stateVersionMetadata) { if (Version.CURRENT.before(stateVersionMetadata.minCompatibleVersion())) { logger.info( @@ -206,7 +212,7 @@ static boolean checkMetadataVersion(OperatorMetadata existingMetadata, OperatorS return true; } - void recordErrorState(String namespace, Long version, List errors, OperatorErrorMetadata.ErrorKind errorKind) { + private void recordErrorState(String namespace, Long version, List errors, OperatorErrorMetadata.ErrorKind errorKind) { clusterService.submitStateUpdateTask( "operator state error for [ " + namespace + "]", new OperatorUpdateErrorTask(new ActionListener<>() { @@ -225,6 +231,7 @@ public void onFailure(Exception e) { ); } + // package private for testing LinkedHashSet orderedStateHandlers(Set keys) { LinkedHashSet orderedHandlers = new LinkedHashSet<>(); LinkedHashSet dependencyStack = new LinkedHashSet<>(); @@ -236,7 +243,7 @@ LinkedHashSet orderedStateHandlers(Set keys) { return orderedHandlers; } - void addStateHandler(String key, Set keys, LinkedHashSet ordered, LinkedHashSet visited) { + private void addStateHandler(String key, Set keys, LinkedHashSet ordered, LinkedHashSet visited) { if (visited.contains(key)) { StringBuilder msg = new StringBuilder("Cycle found in settings dependencies: "); visited.forEach(s -> { diff --git a/server/src/test/java/org/elasticsearch/operator/service/OperatorClusterStateControllerTests.java b/server/src/test/java/org/elasticsearch/operator/service/OperatorClusterStateControllerTests.java index 1ed94cc621c7e..200b9a7bc4e18 100644 --- a/server/src/test/java/org/elasticsearch/operator/service/OperatorClusterStateControllerTests.java +++ b/server/src/test/java/org/elasticsearch/operator/service/OperatorClusterStateControllerTests.java @@ -21,6 +21,8 @@ import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.settings.ClusterSettings; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.operator.OperatorHandler; +import org.elasticsearch.operator.TransformState; import org.elasticsearch.operator.action.OperatorClusterUpdateSettingsAction; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.xcontent.XContentParser; @@ -28,12 +30,16 @@ import org.elasticsearch.xcontent.XContentType; import java.io.IOException; +import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Consumer; +import static org.hamcrest.Matchers.anyOf; import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.is; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; @@ -197,4 +203,99 @@ public void testcheckMetadataVersion() { ) ); } + + public void testHandlerOrdering() { + OperatorHandler oh1 = new OperatorHandler<>() { + @Override + public String key() { + return "one"; + } + + @Override + public TransformState transform(Object source, TransformState prevState) throws Exception { + return null; + } + + @Override + public Collection dependencies() { + return List.of("two", "three"); + } + }; + + OperatorHandler oh2 = new OperatorHandler<>() { + @Override + public String key() { + return "two"; + } + + @Override + public TransformState transform(Object source, TransformState prevState) throws Exception { + return null; + } + }; + + OperatorHandler oh3 = new OperatorHandler<>() { + @Override + public String key() { + return "three"; + } + + @Override + public TransformState transform(Object source, TransformState prevState) throws Exception { + return null; + } + + @Override + public Collection dependencies() { + return List.of("two"); + } + }; + + ClusterService clusterService = mock(ClusterService.class); + OperatorClusterStateController controller = new OperatorClusterStateController(clusterService); + + controller.initHandlers(List.of(oh1, oh2, oh3)); + Collection ordered = controller.orderedStateHandlers(Set.of("one", "two", "three")); + assertThat(ordered, contains("two", "three", "one")); + + // assure that we bail on unknown handler + assertEquals( + "Unknown settings definition type: four", + expectThrows(IllegalStateException.class, () -> controller.orderedStateHandlers(Set.of("one", "two", "three", "four"))) + .getMessage() + ); + + // assure that we bail on missing dependency link + assertEquals( + "Missing settings dependency definition: one -> three", + expectThrows(IllegalStateException.class, () -> controller.orderedStateHandlers(Set.of("one", "two"))).getMessage() + ); + + // Change the second handler so that we create cycle + oh2 = new OperatorHandler<>() { + @Override + public String key() { + return "two"; + } + + @Override + public TransformState transform(Object source, TransformState prevState) throws Exception { + return null; + } + + @Override + public Collection dependencies() { + return List.of("one"); + } + }; + + controller.initHandlers(List.of(oh1, oh2)); + assertThat( + expectThrows(IllegalStateException.class, () -> controller.orderedStateHandlers(Set.of("one", "two"))).getMessage(), + anyOf( + is("Cycle found in settings dependencies: one -> two -> one"), + is("Cycle found in settings dependencies: two -> one -> two") + ) + ); + } } From 45d5a69c97a22aa0a2efce092632bbca89bb1b10 Mon Sep 17 00:00:00 2001 From: Nikola Grcevski Date: Wed, 8 Jun 2022 09:34:51 -0400 Subject: [PATCH 35/48] Add FileSettingsService tests --- .../common/settings/ClusterSettings.java | 2 +- .../operator/service/FileSettingsService.java | 6 +- .../service/FileSettingsServiceTests.java | 145 ++++++++++++++++++ 3 files changed, 149 insertions(+), 4 deletions(-) create mode 100644 server/src/test/java/org/elasticsearch/operator/service/FileSettingsServiceTests.java diff --git a/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java b/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java index 9efc8d287c19a..69c11dcc14f60 100644 --- a/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java +++ b/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java @@ -518,7 +518,7 @@ public void apply(Settings value, Settings current, Settings previous) { StableMasterHealthIndicatorService.NO_MASTER_TRANSITIONS_THRESHOLD_SETTING, MasterHistory.MAX_HISTORY_AGE_SETTING, ReadinessService.PORT, - FileSettingsService.OPERATOR_DIR_NAME + FileSettingsService.OPERATOR_DIRECTORY ); static List> BUILT_IN_SETTING_UPGRADERS = Collections.emptyList(); diff --git a/server/src/main/java/org/elasticsearch/operator/service/FileSettingsService.java b/server/src/main/java/org/elasticsearch/operator/service/FileSettingsService.java index 2b37258e1e3d0..bab17d35400d6 100644 --- a/server/src/main/java/org/elasticsearch/operator/service/FileSettingsService.java +++ b/server/src/main/java/org/elasticsearch/operator/service/FileSettingsService.java @@ -48,8 +48,8 @@ public class FileSettingsService extends AbstractLifecycleComponent implements C private volatile boolean active = false; - public static final Setting OPERATOR_DIR_NAME = Setting.simpleString( - "path.config.operator_dir_name", + public static final Setting OPERATOR_DIRECTORY = Setting.simpleString( + "path.config.operator_directory", "operator", Setting.Property.NodeScope ); @@ -63,7 +63,7 @@ public FileSettingsService(ClusterService clusterService, OperatorClusterStateCo // package private for testing Path operatorSettingsDir() { - String dirPath = OPERATOR_DIR_NAME.get(environment.settings()); + String dirPath = OPERATOR_DIRECTORY.get(environment.settings()); return environment.configFile().toAbsolutePath().resolve(dirPath); } diff --git a/server/src/test/java/org/elasticsearch/operator/service/FileSettingsServiceTests.java b/server/src/test/java/org/elasticsearch/operator/service/FileSettingsServiceTests.java new file mode 100644 index 0000000000000..25ecfcccfd3b6 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/operator/service/FileSettingsServiceTests.java @@ -0,0 +1,145 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.operator.service; + +import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.common.settings.ClusterSettings; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.env.Environment; +import org.elasticsearch.operator.action.OperatorClusterUpdateSettingsAction; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.threadpool.TestThreadPool; +import org.elasticsearch.threadpool.ThreadPool; +import org.junit.After; +import org.junit.Before; +import org.mockito.stubbing.Answer; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.FileTime; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +public class FileSettingsServiceTests extends ESTestCase { + private Environment env; + private ClusterService clusterService; + private FileSettingsService fileSettingsService; + private ThreadPool threadpool; + + @Before + public void setUp() throws Exception { + super.setUp(); + + threadpool = new TestThreadPool("file_settings_service_tests"); + + clusterService = new ClusterService( + Settings.EMPTY, + new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS), + threadpool + ); + env = newEnvironment(Settings.EMPTY); + + Files.createDirectories(env.configFile()); + + ClusterSettings clusterSettings = new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS); + + OperatorClusterStateController controller = new OperatorClusterStateController(clusterService); + controller.initHandlers(List.of(new OperatorClusterUpdateSettingsAction(clusterSettings))); + + fileSettingsService = new FileSettingsService(clusterService, controller, env); + } + + @After + public void tearDown() throws Exception { + super.tearDown(); + threadpool.shutdownNow(); + } + + public void testOperatorDirName() { + Path operatorPath = fileSettingsService.operatorSettingsDir(); + assertTrue(operatorPath.startsWith(env.configFile())); + assertTrue(operatorPath.endsWith("operator")); + + Path operatorSettingsFile = fileSettingsService.operatorSettingsFile(); + assertTrue(operatorSettingsFile.startsWith(operatorPath)); + assertTrue(operatorSettingsFile.endsWith("settings.json")); + } + + public void testWatchedFile() throws Exception { + Path tmpFile = createTempFile(); + Path tmpFile1 = createTempFile(); + Path otherFile = tmpFile.getParent().resolve("other.json"); + // we return false on non-existent paths, we don't remember state + assertFalse(fileSettingsService.watchedFileChanged(otherFile)); + + // we remember the previous state + assertTrue(fileSettingsService.watchedFileChanged(tmpFile)); + assertFalse(fileSettingsService.watchedFileChanged(tmpFile)); + + // we modify the timestamp of the file, it should trigger a change + Instant now = LocalDateTime.now().toInstant(ZoneOffset.ofHours(0)); + Files.setLastModifiedTime(tmpFile, FileTime.from(now)); + + assertTrue(fileSettingsService.watchedFileChanged(tmpFile)); + assertFalse(fileSettingsService.watchedFileChanged(tmpFile)); + + // we change to another real file, it should be changed + assertTrue(fileSettingsService.watchedFileChanged(tmpFile1)); + assertFalse(fileSettingsService.watchedFileChanged(tmpFile1)); + } + + public void testStartStop() { + fileSettingsService.start(); + fileSettingsService.startWatcher(); + assertTrue(fileSettingsService.watching()); + fileSettingsService.stop(); + assertFalse(fileSettingsService.watching()); + fileSettingsService.close(); + } + + public void testCallsProcessing() throws Exception { + FileSettingsService service = spy(fileSettingsService); + CountDownLatch processFileLatch = new CountDownLatch(1); + + doAnswer((Answer) invocation -> { + processFileLatch.countDown(); + return null; + }).when(service).processFileSettings(any()); + + service.start(); + service.startWatcher(); + assertTrue(service.watching()); + + Files.createDirectories(service.operatorSettingsDir()); + + Files.write(service.operatorSettingsFile(), "{}".getBytes(StandardCharsets.UTF_8)); + + // we need to wait a bit, on MacOS it may take up to 10 seconds for the Java watcher service to notice the file, + // on Linux is instantaneous. + processFileLatch.await(30, TimeUnit.SECONDS); + + verify(service, times(1)).watchedFileChanged(any()); + + service.stop(); + assertFalse(service.watching()); + service.close(); + } + +} From 6b6e99bc542a6236543c7e10d3eb9268e47075fd Mon Sep 17 00:00:00 2001 From: Nikola Grcevski Date: Wed, 8 Jun 2022 10:01:07 -0400 Subject: [PATCH 36/48] Document code --- .../operator/OperatorHandler.java | 56 ++++++++++++++++++- .../operator/TransformState.java | 5 +- .../OperatorClusterUpdateSettingsAction.java | 5 +- 3 files changed, 63 insertions(+), 3 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/operator/OperatorHandler.java b/server/src/main/java/org/elasticsearch/operator/OperatorHandler.java index bc25eb790a1a8..b7e5f3010ec37 100644 --- a/server/src/main/java/org/elasticsearch/operator/OperatorHandler.java +++ b/server/src/main/java/org/elasticsearch/operator/OperatorHandler.java @@ -24,19 +24,60 @@ import java.util.Map; /** - * TODO: Add docs + * Updating cluster state in operator mode, for file based settings and modules/plugins, requires + * that we have a separate update handler interface to the REST handlers. This interface declares + * the basic contract for implementing cluster state update handlers in operator mode. */ public interface OperatorHandler> { String CONTENT = "content"; + /** + * The operator handler key is a unique identifier that is matched to a section in a + * cluster state update content. The operator cluster state updates are done as a single + * cluster state update and the cluster state is typically supplied as a combined content, + * unlike the REST handlers. This key must match a desired content key in the combined + * cluster state update, e.g. "ilm" or "cluster_settings" (for persistent cluster settings update). + * + * @return a String with the operator key name + */ String key(); + /** + * The transform method of the operator handler should apply the necessary changes to + * the cluster state as it normally would in a REST handler. One difference is that the + * transform method in an operator handler must perform all CRUD operations of the cluster + * state in one go. For that reason, we supply a wrapper class to the cluster state called + * TransformState, which contains the current cluster state as well as any previous keys + * set by this handler on prior invocation. + * + * @param source The parsed information specific to this handler from the combined cluster state content + * @param prevState The previous cluster state and keys set by this handler (if any) + * @return The modified state and the current keys set by this handler + * @throws Exception + */ TransformState transform(Object source, TransformState prevState) throws Exception; + /** + * Sometimes certain parts of the cluster state cannot be created/updated without previously + * setting other cluster state components, e.g. composable templates. Since the cluster state handlers + * are processed in random order by the OperatorClusterStateController, this method gives an opportunity + * to any operator handler to declare other operator handlers it depends on. Given dependencies exist, + * the OperatorClusterStateController will order those handlers such that the handlers that are dependent + * on are processed first. + * + * @return a collection of operator handler names + */ default Collection dependencies() { return Collections.emptyList(); } + /** + * All implementations of OperatorHandler should call the request validate method, by calling this default + * implementation. To aid in any special validation logic that may need to be implemented by the operator handler + * we provide this convenience method. + * + * @param request the master node request that we base this operator handler on + */ default void validate(T request) { ActionRequestValidationException exception = request.validate(); if (exception != null) { @@ -44,6 +85,12 @@ default void validate(T request) { } } + /** + * Convenience method to convert the incoming passed in input to the transform method into a map. + * + * @param input the input passed into the operator handler after parsing the content + * @return + */ @SuppressWarnings("unchecked") default Map asMap(Object input) { if (input instanceof Map source) { @@ -52,6 +99,13 @@ default void validate(T request) { throw new IllegalStateException("Unsupported " + key() + " request format"); } + /** + * Convenience method that creates a XContentParser from a content map so that it can be passed to + * existing REST based code for input parsing. + * + * @param source the operator content as a map + * @return + */ default XContentParser mapToXContentParser(Map source) { try (XContentBuilder builder = XContentFactory.contentBuilder(XContentType.JSON)) { builder.map(source); diff --git a/server/src/main/java/org/elasticsearch/operator/TransformState.java b/server/src/main/java/org/elasticsearch/operator/TransformState.java index 71b0a3db66407..e93d47f7876b6 100644 --- a/server/src/main/java/org/elasticsearch/operator/TransformState.java +++ b/server/src/main/java/org/elasticsearch/operator/TransformState.java @@ -13,6 +13,9 @@ import java.util.Set; /** - * TODO: Add docs + * A ClusterState wrapper used by the OperatorClusterStateController to pass the + * current state as well as previous keys set by an OperatorHandler to each transform + * step of the cluster state update. + * */ public record TransformState(ClusterState state, Set keys) {} diff --git a/server/src/main/java/org/elasticsearch/operator/action/OperatorClusterUpdateSettingsAction.java b/server/src/main/java/org/elasticsearch/operator/action/OperatorClusterUpdateSettingsAction.java index 94777d2df86e2..e75b1ed208330 100644 --- a/server/src/main/java/org/elasticsearch/operator/action/OperatorClusterUpdateSettingsAction.java +++ b/server/src/main/java/org/elasticsearch/operator/action/OperatorClusterUpdateSettingsAction.java @@ -23,7 +23,10 @@ import java.util.stream.Collectors; /** - * TODO: Add docs + * This Action is the Operator version of RestClusterUpdateSettingsAction + * + * It is used by the OperatorClusterStateController to update the persistent cluster settings. + * Since transient cluster settings are deprecated, this action doesn't support updating cluster settings. */ public class OperatorClusterUpdateSettingsAction implements OperatorHandler { From 813398efcbd052b6557908735829c17f57d82dde Mon Sep 17 00:00:00 2001 From: Nikola Grcevski Date: Wed, 8 Jun 2022 10:35:03 -0400 Subject: [PATCH 37/48] Add TransportMasterNodeActionTests unit tests --- .../TransportMasterNodeActionTests.java | 102 +++++++++++++++++- 1 file changed, 101 insertions(+), 1 deletion(-) diff --git a/server/src/test/java/org/elasticsearch/action/support/master/TransportMasterNodeActionTests.java b/server/src/test/java/org/elasticsearch/action/support/master/TransportMasterNodeActionTests.java index 9770b1c42dc0f..47dd735e7cebd 100644 --- a/server/src/test/java/org/elasticsearch/action/support/master/TransportMasterNodeActionTests.java +++ b/server/src/test/java/org/elasticsearch/action/support/master/TransportMasterNodeActionTests.java @@ -14,12 +14,14 @@ import org.elasticsearch.action.ActionRequestValidationException; import org.elasticsearch.action.ActionResponse; import org.elasticsearch.action.IndicesRequest; +import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsRequest; import org.elasticsearch.action.support.ActionFilters; import org.elasticsearch.action.support.ActionTestUtils; import org.elasticsearch.action.support.IndicesOptions; import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.action.support.ThreadedActionListener; import org.elasticsearch.action.support.replication.ClusterStateCreationUtils; +import org.elasticsearch.cluster.ClusterName; import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.NotMasterException; import org.elasticsearch.cluster.block.ClusterBlock; @@ -30,6 +32,8 @@ import org.elasticsearch.cluster.metadata.IndexMetadata; import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; import org.elasticsearch.cluster.metadata.Metadata; +import org.elasticsearch.cluster.metadata.OperatorHandlerMetadata; +import org.elasticsearch.cluster.metadata.OperatorMetadata; import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.cluster.node.DiscoveryNodeRole; import org.elasticsearch.cluster.node.DiscoveryNodes; @@ -43,6 +47,7 @@ import org.elasticsearch.discovery.MasterNotDiscoveredException; import org.elasticsearch.indices.TestIndexNameExpressionResolver; import org.elasticsearch.node.NodeClosedException; +import org.elasticsearch.operator.action.OperatorClusterUpdateSettingsAction; import org.elasticsearch.rest.RestStatus; import org.elasticsearch.tasks.CancellableTask; import org.elasticsearch.tasks.Task; @@ -65,6 +70,7 @@ import java.util.HashSet; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.CyclicBarrier; @@ -252,6 +258,57 @@ protected void masterOperation(Task task, Request request, ClusterState state, A protected ClusterBlockException checkBlock(Request request, ClusterState state) { return null; // default implementation, overridden in specific tests } + + @Override + protected Optional operatorHandlerName() { + return Optional.of("test_operator"); + } + } + + class FakeClusterStateUpdateAction extends TransportMasterNodeAction { + FakeClusterStateUpdateAction( + String actionName, + TransportService transportService, + ClusterService clusterService, + ThreadPool threadPool, + String executor + ) { + super( + actionName, + transportService, + clusterService, + threadPool, + new ActionFilters(new HashSet<>()), + ClusterUpdateSettingsRequest::new, + TestIndexNameExpressionResolver.newInstance(), + Response::new, + executor + ); + } + + @Override + protected void masterOperation( + Task task, + ClusterUpdateSettingsRequest request, + ClusterState state, + ActionListener listener + ) {} + + @Override + protected ClusterBlockException checkBlock(ClusterUpdateSettingsRequest request, ClusterState state) { + return null; + } + + @Override + protected Optional operatorHandlerName() { + return Optional.of(OperatorClusterUpdateSettingsAction.KEY); + } + + @Override + protected Set modifiedKeys(ClusterUpdateSettingsRequest request) { + Settings allSettings = Settings.builder().put(request.persistentSettings()).put(request.transientSettings()).build(); + return allSettings.keySet(); + } } public void testLocalOperationWithoutBlocks() throws ExecutionException, InterruptedException { @@ -686,7 +743,6 @@ protected ClusterBlockException checkBlock(Request request, ClusterState state) indexNameExpressionResolver.concreteIndexNamesWithSystemIndexAccess(state, request) ); } - }; PlainActionFuture listener = new PlainActionFuture<>(); @@ -697,6 +753,50 @@ protected ClusterBlockException checkBlock(Request request, ClusterState state) assertThat(ex.getCause().getCause(), instanceOf(ClusterBlockException.class)); } + public void testRejectOperatorConflictClusterStateUpdate() { + OperatorHandlerMetadata hmOne = new OperatorHandlerMetadata.Builder(OperatorClusterUpdateSettingsAction.KEY).keys(Set.of("a", "b")) + .build(); + OperatorHandlerMetadata hmThree = new OperatorHandlerMetadata.Builder(OperatorClusterUpdateSettingsAction.KEY).keys( + Set.of("e", "f") + ).build(); + + OperatorMetadata omOne = OperatorMetadata.builder("namespace_one").putHandler(hmOne).build(); + OperatorMetadata omTwo = OperatorMetadata.builder("namespace_two").putHandler(hmThree).build(); + + Metadata metadata = Metadata.builder().putOperatorState(omOne).putOperatorState(omTwo).build(); + + ClusterState clusterState = ClusterState.builder(new ClusterName("test")).metadata(metadata).build(); + + Action noHandler = new Action("internal:testAction", transportService, clusterService, threadPool, ThreadPool.Names.SAME); + + // nothing should happen here, since the request doesn't touch any of the operator key + noHandler.validateForOperatorState(new Request(), clusterState); + + ClusterUpdateSettingsRequest request = new ClusterUpdateSettingsRequest().persistentSettings( + Settings.builder().put("a", "a value").build() + ).transientSettings(Settings.builder().put("e", "e value").build()); + + FakeClusterStateUpdateAction action = new FakeClusterStateUpdateAction( + "internal:testClusterSettings", + transportService, + clusterService, + threadPool, + ThreadPool.Names.SAME + ); + + assertTrue( + expectThrows(IllegalStateException.class, () -> action.validateForOperatorState(request, clusterState)).getMessage() + .contains("with errors: [a] set in operator mode by [namespace_one]\n" + "[e] set in operator mode by [namespace_two]") + ); + + ClusterUpdateSettingsRequest okRequest = new ClusterUpdateSettingsRequest().persistentSettings( + Settings.builder().put("m", "m value").build() + ).transientSettings(Settings.builder().put("n", "n value").build()); + + // this should just work, no conflicts + action.validateForOperatorState(okRequest, clusterState); + } + private Runnable blockAllThreads(String executorName) throws Exception { final int numberOfThreads = threadPool.info(executorName).getMax(); final EsThreadPoolExecutor executor = (EsThreadPoolExecutor) threadPool.executor(executorName); From 77dd3d04d34168881555e4c1acc2446e744f839b Mon Sep 17 00:00:00 2001 From: Nikola Grcevski Date: Wed, 8 Jun 2022 12:57:35 -0400 Subject: [PATCH 38/48] Add more tests and fix issues --- .../cluster/metadata/Metadata.java | 2 +- .../metadata/OperatorErrorMetadata.java | 35 +++----- .../metadata/OperatorHandlerMetadata.java | 21 +---- .../cluster/metadata/OperatorMetadata.java | 83 +++++++++--------- .../operator/OperatorHandler.java | 4 +- .../operator/TransformState.java | 4 +- .../metadata/OperatorMetadataTests.java | 86 +++++++++++++++++++ .../metadata/ToAndFromJsonMetadataTests.java | 23 +++++ .../service/FileSettingsServiceTests.java | 3 +- .../org/elasticsearch/test/ESTestCase.java | 4 + 10 files changed, 173 insertions(+), 92 deletions(-) create mode 100644 server/src/test/java/org/elasticsearch/cluster/metadata/OperatorMetadataTests.java diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/Metadata.java b/server/src/main/java/org/elasticsearch/cluster/metadata/Metadata.java index 2e4d7396ad4a7..04afc1ca1523a 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/Metadata.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/Metadata.java @@ -2263,7 +2263,7 @@ public static Metadata fromXContent(XContentParser parser) throws IOException { } } else if ("operator".equals(currentFieldName)) { while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { - builder.putOperatorState(OperatorMetadata.Builder.fromXContent(parser, parser.currentName())); + builder.putOperatorState(OperatorMetadata.fromXContent(parser)); } } else { try { diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/OperatorErrorMetadata.java b/server/src/main/java/org/elasticsearch/cluster/metadata/OperatorErrorMetadata.java index 7a7dd406da1bc..c1a15859bc509 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/OperatorErrorMetadata.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/OperatorErrorMetadata.java @@ -24,37 +24,22 @@ /** * Metadata class to hold error information about errors encountered * while applying a cluster state update for a given namespace. - * + *

* This information is held by the OperatorMetadata class. */ -public class OperatorErrorMetadata implements SimpleDiffable, ToXContentFragment { - private final Long version; - private final ErrorKind errorKind; - private final List errors; - +public record OperatorErrorMetadata( + Long version, + org.elasticsearch.cluster.metadata.OperatorErrorMetadata.ErrorKind errorKind, + List errors +) implements SimpleDiffable, ToXContentFragment { /** * Contructs an operator metadata - * @param version the metadata version which failed to apply + * + * @param version the metadata version which failed to apply * @param errorKind the kind of error we encountered while processing - * @param errors the list of errors encountered during parsing and validation of the metadata + * @param errors the list of errors encountered during parsing and validation of the metadata */ - public OperatorErrorMetadata(Long version, ErrorKind errorKind, List errors) { - this.version = version; - this.errors = errors; - this.errorKind = errorKind; - } - - public Long version() { - return this.version; - } - - public List errors() { - return this.errors; - } - - public ErrorKind errorKind() { - return errorKind; - } + public OperatorErrorMetadata {} @Override public void writeTo(StreamOutput out) throws IOException { diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/OperatorHandlerMetadata.java b/server/src/main/java/org/elasticsearch/cluster/metadata/OperatorHandlerMetadata.java index a933fef264633..5a8947d03a7cd 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/OperatorHandlerMetadata.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/OperatorHandlerMetadata.java @@ -24,24 +24,11 @@ /** * Metadata class to hold the operator set keys for each operator handler - * */ -public class OperatorHandlerMetadata implements SimpleDiffable, ToXContentFragment { - private final String name; - private final Set keys; - - public OperatorHandlerMetadata(String name, Set keys) { - this.name = name; - this.keys = keys; - } - - public String name() { - return this.name; - } - - public Set keys() { - return this.keys; - } +public record OperatorHandlerMetadata(String name, Set keys) + implements + SimpleDiffable, + ToXContentFragment { @Override public void writeTo(StreamOutput out) throws IOException { diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/OperatorMetadata.java b/server/src/main/java/org/elasticsearch/cluster/metadata/OperatorMetadata.java index 481d8ab10863c..78f55e9a426d3 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/OperatorMetadata.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/OperatorMetadata.java @@ -15,6 +15,7 @@ import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.xcontent.ToXContent; +import org.elasticsearch.xcontent.ToXContentFragment; import org.elasticsearch.xcontent.XContentBuilder; import org.elasticsearch.xcontent.XContentParser; @@ -31,49 +32,23 @@ * in an operator mode. These types of settings are read only through the REST API, * and cannot be modified by the end user. */ -public class OperatorMetadata implements SimpleDiffable { - private final String namespace; - private final Long version; - private final Map handlers; - private final OperatorErrorMetadata errorMetadata; - +public record OperatorMetadata( + String namespace, + Long version, + Map handlers, + OperatorErrorMetadata errorMetadata +) implements SimpleDiffable, ToXContentFragment { /** * OperatorMetadata contains information about settings set in operator mode. * These settings cannot be updated by the end user and are set outside of the * REST layer, e.g. through file based settings or by plugin/modules. * - * @param namespace The namespace of the setting creator, e.g. file_settings, security plugin, etc. - * @param version The update version, must increase with each update - * @param handlers Per state update handler information on key set in by this update. These keys are validated at REST time. + * @param namespace The namespace of the setting creator, e.g. file_settings, security plugin, etc. + * @param version The update version, must increase with each update + * @param handlers Per state update handler information on key set in by this update. These keys are validated at REST time. * @param errorMetadata If the update failed for some reason, this is where we store the error information metadata. */ - public OperatorMetadata( - String namespace, - Long version, - Map handlers, - OperatorErrorMetadata errorMetadata - ) { - this.namespace = namespace; - this.version = version; - this.handlers = handlers; - this.errorMetadata = errorMetadata; - } - - public String namespace() { - return namespace; - } - - public Long version() { - return version; - } - - public OperatorErrorMetadata errorMetadata() { - return errorMetadata; - } - - public Map handlers() { - return handlers; - } + public OperatorMetadata {} public Set conflicts(String handlerName, Set modified) { OperatorHandlerMetadata handlerMetadata = handlers.get(handlerName); @@ -127,6 +102,17 @@ public static Builder builder(String namespace, OperatorMetadata metadata) { return new Builder(namespace, metadata); } + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + Builder.toXContent(this, builder, params); + return builder; + } + + public static OperatorMetadata fromXContent(final XContentParser parser) throws IOException { + parser.nextToken(); + return Builder.fromXContent(parser, parser.currentName()); + } + /** * Builder class for OperatorMetadata */ @@ -142,6 +128,7 @@ public static class Builder { /** * Empty builder for OperatorMetadata + * * @param namespace The namespace for this metadata */ public Builder(String namespace) { @@ -153,14 +140,15 @@ public Builder(String namespace) { /** * Creates an operator metadata builder + * * @param namespace the namespace for which we are storing metadata, e.g. file_settings - * @param metadata the previous metadata + * @param metadata the previous metadata */ public Builder(String namespace, OperatorMetadata metadata) { this(namespace); if (metadata != null) { this.version = metadata.version; - this.handlers = metadata.handlers; + this.handlers = new HashMap<>(metadata.handlers); this.errorMetadata = metadata.errorMetadata; } } @@ -175,11 +163,6 @@ public Builder errorMetadata(OperatorErrorMetadata errorMetadata) { return this; } - public Builder handlerKeys(Map handlers) { - this.handlers = handlers; - return this; - } - public Builder putHandler(OperatorHandlerMetadata handler) { this.handlers.put(handler.name(), handler); return this; @@ -220,7 +203,7 @@ public static void toXContent(OperatorMetadata operatorMetadata, XContentBuilder public static OperatorMetadata fromXContent(XContentParser parser, String namespace) throws IOException { OperatorMetadata.Builder builder = new OperatorMetadata.Builder(namespace); - String currentFieldName = parser.currentName(); + String currentFieldName = skipNamespace(parser); XContentParser.Token token; while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { if (token == XContentParser.Token.FIELD_NAME) { @@ -243,5 +226,17 @@ public static OperatorMetadata fromXContent(XContentParser parser, String namesp } return builder.build(); } + + private static String skipNamespace(XContentParser parser) throws IOException { + XContentParser.Token token = parser.nextToken(); + if (token == XContentParser.Token.START_OBJECT) { + token = parser.nextToken(); + if (token == XContentParser.Token.FIELD_NAME) { + return parser.currentName(); + } + } + + return null; + } } } diff --git a/server/src/main/java/org/elasticsearch/operator/OperatorHandler.java b/server/src/main/java/org/elasticsearch/operator/OperatorHandler.java index b7e5f3010ec37..cdd33c9f53f98 100644 --- a/server/src/main/java/org/elasticsearch/operator/OperatorHandler.java +++ b/server/src/main/java/org/elasticsearch/operator/OperatorHandler.java @@ -47,7 +47,7 @@ public interface OperatorHandler> { * the cluster state as it normally would in a REST handler. One difference is that the * transform method in an operator handler must perform all CRUD operations of the cluster * state in one go. For that reason, we supply a wrapper class to the cluster state called - * TransformState, which contains the current cluster state as well as any previous keys + * {@link TransformState}, which contains the current cluster state as well as any previous keys * set by this handler on prior invocation. * * @param source The parsed information specific to this handler from the combined cluster state content @@ -100,7 +100,7 @@ default void validate(T request) { } /** - * Convenience method that creates a XContentParser from a content map so that it can be passed to + * Convenience method that creates a {@link XContentParser} from a content map so that it can be passed to * existing REST based code for input parsing. * * @param source the operator content as a map diff --git a/server/src/main/java/org/elasticsearch/operator/TransformState.java b/server/src/main/java/org/elasticsearch/operator/TransformState.java index e93d47f7876b6..a68ce7e1f2290 100644 --- a/server/src/main/java/org/elasticsearch/operator/TransformState.java +++ b/server/src/main/java/org/elasticsearch/operator/TransformState.java @@ -13,8 +13,8 @@ import java.util.Set; /** - * A ClusterState wrapper used by the OperatorClusterStateController to pass the - * current state as well as previous keys set by an OperatorHandler to each transform + * A {@link ClusterState} wrapper used by the OperatorClusterStateController to pass the + * current state as well as previous keys set by an {@link OperatorHandler} to each transform * step of the cluster state update. * */ diff --git a/server/src/test/java/org/elasticsearch/cluster/metadata/OperatorMetadataTests.java b/server/src/test/java/org/elasticsearch/cluster/metadata/OperatorMetadataTests.java new file mode 100644 index 0000000000000..63464d0e4c0b2 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/cluster/metadata/OperatorMetadataTests.java @@ -0,0 +1,86 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.cluster.metadata; + +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.io.stream.BytesStreamOutput; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xcontent.ToXContent; +import org.elasticsearch.xcontent.XContentBuilder; +import org.elasticsearch.xcontent.XContentParser; +import org.elasticsearch.xcontent.json.JsonXContent; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.not; + +/** + * Tests for the {@link OperatorMetadata}, {@link OperatorErrorMetadata}, {@link OperatorHandlerMetadata} classes + */ +public class OperatorMetadataTests extends ESTestCase { + + public void testEquals() { + final OperatorMetadata meta = createRandom(); + assertThat(meta, equalTo(OperatorMetadata.builder(meta.namespace(), meta).build())); + final OperatorMetadata.Builder newMeta = OperatorMetadata.builder(meta.namespace(), meta); + newMeta.putHandler(new OperatorHandlerMetadata("1", Collections.emptySet())); + assertThat(newMeta.build(), not(meta)); + } + + public void testSerialization() throws IOException { + final OperatorMetadata meta = createRandom(); + final BytesStreamOutput out = new BytesStreamOutput(); + meta.writeTo(out); + assertThat(OperatorMetadata.readFrom(out.bytes().streamInput()), equalTo(meta)); + } + + public void testXContent() throws IOException { + final OperatorMetadata meta = createRandom(); + final XContentBuilder builder = JsonXContent.contentBuilder(); + builder.startObject(); + meta.toXContent(builder, ToXContent.EMPTY_PARAMS); + builder.endObject(); + XContentParser parser = createParser(JsonXContent.jsonXContent, BytesReference.bytes(builder)); + parser.nextToken(); // the beginning of the object + assertThat(OperatorMetadata.fromXContent(parser), equalTo(meta)); + } + + private static OperatorMetadata createRandom() { + List handlers = randomList( + 0, + 10, + () -> new OperatorHandlerMetadata.Builder(randomAlphaOfLength(5)).keys(randomSet(1, 5, () -> randomAlphaOfLength(6))).build() + ); + + List errors = randomList( + 0, + 10, + () -> new OperatorErrorMetadata.Builder().version(1L) + .errorKind(randomFrom(OperatorErrorMetadata.ErrorKind.values())) + .errors(randomList(1, 5, () -> randomAlphaOfLength(10))) + .build() + ); + + OperatorMetadata.Builder builder = OperatorMetadata.builder(randomAlphaOfLength(7)); + + for (var handlerMeta : handlers) { + builder.putHandler(handlerMeta); + } + + for (var errorMeta : errors) { + builder.errorMetadata(errorMeta); + } + + return builder.build(); + } + +} diff --git a/server/src/test/java/org/elasticsearch/cluster/metadata/ToAndFromJsonMetadataTests.java b/server/src/test/java/org/elasticsearch/cluster/metadata/ToAndFromJsonMetadataTests.java index 2f7500be70a75..c85d9bd5c2cae 100644 --- a/server/src/test/java/org/elasticsearch/cluster/metadata/ToAndFromJsonMetadataTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/metadata/ToAndFromJsonMetadataTests.java @@ -45,6 +45,23 @@ public class ToAndFromJsonMetadataTests extends ESTestCase { public void testSimpleJsonFromAndTo() throws IOException { IndexMetadata idx1 = createFirstBackingIndex("data-stream1").build(); IndexMetadata idx2 = createFirstBackingIndex("data-stream2").build(); + + OperatorHandlerMetadata hmOne = new OperatorHandlerMetadata.Builder("one").keys(Set.of("a", "b")).build(); + OperatorHandlerMetadata hmTwo = new OperatorHandlerMetadata.Builder("two").keys(Set.of("c", "d")).build(); + + OperatorErrorMetadata emOne = new OperatorErrorMetadata.Builder().version(1L) + .errorKind(OperatorErrorMetadata.ErrorKind.VALIDATION) + .errors(List.of("Test error 1", "Test error 2")) + .build(); + + OperatorMetadata operatorMetadata = OperatorMetadata.builder("namespace_one") + .errorMetadata(emOne) + .putHandler(hmOne) + .putHandler(hmTwo) + .build(); + + OperatorMetadata operatorMetadata1 = OperatorMetadata.builder("namespace_two").putHandler(hmTwo).build(); + Metadata metadata = Metadata.builder() .put( IndexTemplateMetadata.builder("foo") @@ -108,6 +125,8 @@ public void testSimpleJsonFromAndTo() throws IOException { .put(idx2, false) .put(DataStreamTestHelper.newInstance("data-stream1", List.of(idx1.getIndex()))) .put(DataStreamTestHelper.newInstance("data-stream2", List.of(idx2.getIndex()))) + .putOperatorState(operatorMetadata) + .putOperatorState(operatorMetadata1) .build(); XContentBuilder builder = JsonXContent.contentBuilder(); @@ -181,6 +200,10 @@ public void testSimpleJsonFromAndTo() throws IOException { assertThat(parsedMetadata.dataStreams().get("data-stream2").getName(), is("data-stream2")); assertThat(parsedMetadata.dataStreams().get("data-stream2").getTimeStampField().getName(), is("@timestamp")); assertThat(parsedMetadata.dataStreams().get("data-stream2").getIndices(), contains(idx2.getIndex())); + + // operator metadata + assertEquals(operatorMetadata, parsedMetadata.operatorState(operatorMetadata.namespace())); + assertEquals(operatorMetadata1, parsedMetadata.operatorState(operatorMetadata1.namespace())); } private static final String MAPPING_SOURCE1 = """ diff --git a/server/src/test/java/org/elasticsearch/operator/service/FileSettingsServiceTests.java b/server/src/test/java/org/elasticsearch/operator/service/FileSettingsServiceTests.java index 25ecfcccfd3b6..4f1c263acee2c 100644 --- a/server/src/test/java/org/elasticsearch/operator/service/FileSettingsServiceTests.java +++ b/server/src/test/java/org/elasticsearch/operator/service/FileSettingsServiceTests.java @@ -26,6 +26,7 @@ import java.nio.file.attribute.FileTime; import java.time.Instant; import java.time.LocalDateTime; +import java.time.ZoneId; import java.time.ZoneOffset; import java.util.List; import java.util.concurrent.CountDownLatch; @@ -94,7 +95,7 @@ public void testWatchedFile() throws Exception { assertFalse(fileSettingsService.watchedFileChanged(tmpFile)); // we modify the timestamp of the file, it should trigger a change - Instant now = LocalDateTime.now().toInstant(ZoneOffset.ofHours(0)); + Instant now = LocalDateTime.now(ZoneId.systemDefault()).toInstant(ZoneOffset.ofHours(0)); Files.setLastModifiedTime(tmpFile, FileTime.from(now)); assertTrue(fileSettingsService.watchedFileChanged(tmpFile)); diff --git a/test/framework/src/main/java/org/elasticsearch/test/ESTestCase.java b/test/framework/src/main/java/org/elasticsearch/test/ESTestCase.java index 2b1796aded91e..f1990e0fce0a7 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/ESTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/test/ESTestCase.java @@ -989,6 +989,10 @@ public static Map randomMap(int minMapSize, int maxMapSize, Supplie return list; } + public static Set randomSet(int minSetSize, int maxSetSize, Supplier valueConstructor) { + return new HashSet<>(randomList(minSetSize, maxSetSize, valueConstructor)); + } + private static final String[] TIME_SUFFIXES = new String[] { "d", "h", "ms", "s", "m", "micros", "nanos" }; public static String randomTimeValue(int lower, int upper, String... suffixes) { From fd9a1a02fbf96078f191fa075bb1e7db30de0441 Mon Sep 17 00:00:00 2001 From: Nikola Grcevski Date: Wed, 8 Jun 2022 16:14:38 -0400 Subject: [PATCH 39/48] Apply PR suggestions --- .../TransportClusterUpdateSettingsAction.java | 2 +- .../operator/OperatorHandler.java | 8 +- .../OperatorClusterUpdateSettingsAction.java | 6 +- .../OperatorClusterStateController.java | 114 ++++++----------- .../service/OperatorUpdateErrorTask.java | 63 ++++++---- .../service/OperatorUpdateStateTask.java | 101 ++++++++++++++- .../ClusterUpdateSettingsRequestTests.java | 2 +- .../TransportMasterNodeActionTests.java | 6 +- .../OperatorClusterStateControllerTests.java | 119 +++++++++++++++--- .../TransportDeleteLifecycleAction.java | 2 +- .../action/TransportPutLifecycleAction.java | 2 +- .../operator/OperatorLifecycleAction.java | 6 +- .../TransportDeleteLifecycleActionTests.java | 2 +- .../TransportPutLifecycleActionTests.java | 2 +- 14 files changed, 292 insertions(+), 143 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/settings/TransportClusterUpdateSettingsAction.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/settings/TransportClusterUpdateSettingsAction.java index dfaf552df311b..b106fd45e6dc2 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/cluster/settings/TransportClusterUpdateSettingsAction.java +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/settings/TransportClusterUpdateSettingsAction.java @@ -132,7 +132,7 @@ private static boolean checkClearedBlockAndArchivedSettings( @Override protected Optional operatorHandlerName() { - return Optional.of(OperatorClusterUpdateSettingsAction.KEY); + return Optional.of(OperatorClusterUpdateSettingsAction.NAME); } @Override diff --git a/server/src/main/java/org/elasticsearch/operator/OperatorHandler.java b/server/src/main/java/org/elasticsearch/operator/OperatorHandler.java index cdd33c9f53f98..61ee998541c13 100644 --- a/server/src/main/java/org/elasticsearch/operator/OperatorHandler.java +++ b/server/src/main/java/org/elasticsearch/operator/OperatorHandler.java @@ -32,15 +32,15 @@ public interface OperatorHandler> { String CONTENT = "content"; /** - * The operator handler key is a unique identifier that is matched to a section in a + * The operator handler name is a unique identifier that is matched to a section in a * cluster state update content. The operator cluster state updates are done as a single * cluster state update and the cluster state is typically supplied as a combined content, - * unlike the REST handlers. This key must match a desired content key in the combined + * unlike the REST handlers. This name must match a desired content key name in the combined * cluster state update, e.g. "ilm" or "cluster_settings" (for persistent cluster settings update). * * @return a String with the operator key name */ - String key(); + String name(); /** * The transform method of the operator handler should apply the necessary changes to @@ -96,7 +96,7 @@ default void validate(T request) { if (input instanceof Map source) { return (Map) source; } - throw new IllegalStateException("Unsupported " + key() + " request format"); + throw new IllegalStateException("Unsupported " + name() + " request format"); } /** diff --git a/server/src/main/java/org/elasticsearch/operator/action/OperatorClusterUpdateSettingsAction.java b/server/src/main/java/org/elasticsearch/operator/action/OperatorClusterUpdateSettingsAction.java index e75b1ed208330..ecf2a4f605916 100644 --- a/server/src/main/java/org/elasticsearch/operator/action/OperatorClusterUpdateSettingsAction.java +++ b/server/src/main/java/org/elasticsearch/operator/action/OperatorClusterUpdateSettingsAction.java @@ -30,7 +30,7 @@ */ public class OperatorClusterUpdateSettingsAction implements OperatorHandler { - public static final String KEY = "cluster_settings"; + public static final String NAME = "cluster_settings"; private final ClusterSettings clusterSettings; @@ -39,8 +39,8 @@ public OperatorClusterUpdateSettingsAction(ClusterSettings clusterSettings) { } @Override - public String key() { - return KEY; + public String name() { + return NAME; } @SuppressWarnings("unchecked") diff --git a/server/src/main/java/org/elasticsearch/operator/service/OperatorClusterStateController.java b/server/src/main/java/org/elasticsearch/operator/service/OperatorClusterStateController.java index d7f3a18e2eacc..f069517e972d8 100644 --- a/server/src/main/java/org/elasticsearch/operator/service/OperatorClusterStateController.java +++ b/server/src/main/java/org/elasticsearch/operator/service/OperatorClusterStateController.java @@ -15,20 +15,15 @@ import org.elasticsearch.action.ActionResponse; import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.ClusterStateTaskConfig; -import org.elasticsearch.cluster.metadata.Metadata; import org.elasticsearch.cluster.metadata.OperatorErrorMetadata; -import org.elasticsearch.cluster.metadata.OperatorHandlerMetadata; import org.elasticsearch.cluster.metadata.OperatorMetadata; import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.Priority; import org.elasticsearch.operator.OperatorHandler; -import org.elasticsearch.operator.TransformState; import org.elasticsearch.xcontent.ConstructingObjectParser; import org.elasticsearch.xcontent.ParseField; import org.elasticsearch.xcontent.XContentParser; -import java.util.ArrayList; -import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -36,8 +31,6 @@ import java.util.function.Function; import java.util.stream.Collectors; -import static org.elasticsearch.core.Strings.format; - /** * Controller class for applying file based settings to ClusterState. * This class contains the logic about validation, ordering and applying of @@ -62,10 +55,10 @@ public OperatorClusterStateController(ClusterService clusterService) { * @param handlerList the list of supported operator handlers */ public void initHandlers(List> handlerList) { - handlers = handlerList.stream().collect(Collectors.toMap(OperatorHandler::key, Function.identity())); + handlers = handlerList.stream().collect(Collectors.toMap(OperatorHandler::name, Function.identity())); } - private static class SettingsFile { + static class SettingsFile { public static final ParseField STATE_FIELD = new ParseField("state"); public static final ParseField METADATA_FIELD = new ParseField("metadata"); @SuppressWarnings("unchecked") @@ -92,17 +85,16 @@ private static class SettingsFile { * * @param namespace the namespace under which we'll store the operator keys in the cluster state metadata * @param parser the XContentParser to process - * @return the modified cluster state. If applying the cluster state fails the previous state might be returned. * @throws IllegalStateException if the content has errors and the cluster state cannot be correctly applied */ - public ClusterState process(String namespace, XContentParser parser) { + public void process(String namespace, XContentParser parser) { SettingsFile operatorStateFileContent; try { operatorStateFileContent = SettingsFile.PARSER.apply(parser, null); } catch (Exception e) { List errors = List.of(e.getMessage()); - recordErrorState(namespace, -1L, errors, OperatorErrorMetadata.ErrorKind.PARSING); + recordErrorState(new OperatorErrorState(namespace, -1L, errors, OperatorErrorMetadata.ErrorKind.PARSING)); logger.error("Error processing state change request for [{}] with the following errors [{}]", namespace, errors); throw new IllegalStateException("Error processing state change request for " + namespace, e); @@ -116,7 +108,9 @@ public ClusterState process(String namespace, XContentParser parser) { orderedHandlers = orderedStateHandlers(operatorState.keySet()); } catch (Exception e) { List errors = List.of(e.getMessage()); - recordErrorState(namespace, stateVersionMetadata.version(), errors, OperatorErrorMetadata.ErrorKind.PARSING); + recordErrorState( + new OperatorErrorState(namespace, stateVersionMetadata.version(), errors, OperatorErrorMetadata.ErrorKind.PARSING) + ); logger.error("Error processing state change request for [{}] with the following errors [{}]", namespace, errors); throw new IllegalStateException("Error processing state change request for " + namespace, e); @@ -125,69 +119,33 @@ public ClusterState process(String namespace, XContentParser parser) { ClusterState state = clusterService.state(); OperatorMetadata existingMetadata = state.metadata().operatorState(namespace); if (checkMetadataVersion(existingMetadata, stateVersionMetadata) == false) { - return state; - } - - OperatorMetadata.Builder operatorMetadataBuilder = new OperatorMetadata.Builder(namespace).version(stateVersionMetadata.version()); - List errors = new ArrayList<>(); - - for (var handlerKey : orderedHandlers) { - OperatorHandler handler = handlers.get(handlerKey); - try { - Set existingKeys = keysForHandler(existingMetadata, handlerKey); - TransformState transformState = handler.transform(operatorState.get(handlerKey), new TransformState(state, existingKeys)); - state = transformState.state(); - operatorMetadataBuilder.putHandler(new OperatorHandlerMetadata.Builder(handlerKey).keys(transformState.keys()).build()); - } catch (Exception e) { - errors.add(format("Error processing %s state change: %s", handler.key(), e.getMessage())); - } - } - - if (errors.isEmpty() == false) { - recordErrorState(namespace, stateVersionMetadata.version(), errors, OperatorErrorMetadata.ErrorKind.VALIDATION); - logger.error("Error processing state change request for [{}] with the following errors [{}]", namespace, errors); - - throw new IllegalStateException("Error processing state change request for " + namespace); + return; } - // remove the last error if we had previously encountered any - operatorMetadataBuilder.errorMetadata(null); - - ClusterState.Builder stateBuilder = new ClusterState.Builder(state); - Metadata.Builder metadataBuilder = Metadata.builder(state.metadata()).putOperatorState(operatorMetadataBuilder.build()); - state = stateBuilder.metadata(metadataBuilder).build(); - - // Do we need to retry this? - clusterService.submitStateUpdateTask("operator state [" + namespace + "]", new OperatorUpdateStateTask(new ActionListener<>() { - @Override - public void onResponse(ActionResponse.Empty empty) { - logger.info("Successfully applied new cluster state for namespace [{}]", namespace); - } - - @Override - public void onFailure(Exception e) { - logger.error("Failed to apply operator cluster state", e); - recordErrorState( - namespace, - stateVersionMetadata.version(), - List.of(e.getMessage()), - OperatorErrorMetadata.ErrorKind.TRANSIENT - ); - } - }), + // Do we need to retry this, or it retries automatically? + clusterService.submitStateUpdateTask( + "operator state [" + namespace + "]", + new OperatorUpdateStateTask( + namespace, + operatorStateFileContent, + handlers, + orderedHandlers, + (errorState) -> recordErrorState(errorState), + new ActionListener<>() { + @Override + public void onResponse(ActionResponse.Empty empty) { + logger.info("Successfully applied new cluster state for namespace [{}]", namespace); + } + + @Override + public void onFailure(Exception e) { + logger.error("Failed to apply operator cluster state", e); + } + } + ), ClusterStateTaskConfig.build(Priority.URGENT), - new OperatorUpdateStateTask.OperatorUpdateStateTaskExecutor(namespace, state, clusterService.getRerouteService()) + new OperatorUpdateStateTask.OperatorUpdateStateTaskExecutor(namespace, clusterService.getRerouteService()) ); - - return state; - } - - private Set keysForHandler(OperatorMetadata operatorMetadata, String handlerKey) { - if (operatorMetadata == null || operatorMetadata.handlers().get(handlerKey) == null) { - return Collections.emptySet(); - } - - return operatorMetadata.handlers().get(handlerKey).keys(); } // package private for testing @@ -212,13 +170,15 @@ static boolean checkMetadataVersion(OperatorMetadata existingMetadata, OperatorS return true; } - private void recordErrorState(String namespace, Long version, List errors, OperatorErrorMetadata.ErrorKind errorKind) { + record OperatorErrorState(String namespace, Long version, List errors, OperatorErrorMetadata.ErrorKind errorKind) {} + + private void recordErrorState(OperatorErrorState state) { clusterService.submitStateUpdateTask( - "operator state error for [ " + namespace + "]", - new OperatorUpdateErrorTask(new ActionListener<>() { + "operator state error for [ " + state.namespace + "]", + new OperatorUpdateErrorTask(state, new ActionListener<>() { @Override public void onResponse(ActionResponse.Empty empty) { - logger.info("Successfully applied new operator error state for namespace [{}]", namespace); + logger.info("Successfully applied new operator error state for namespace [{}]", state.namespace); } @Override @@ -227,7 +187,7 @@ public void onFailure(Exception e) { } }), ClusterStateTaskConfig.build(Priority.URGENT), - new OperatorUpdateErrorTask.OperatorUpdateErrorTaskExecutor(namespace, version, errorKind, errors) + new OperatorUpdateErrorTask.OperatorUpdateErrorTaskExecutor() ); } diff --git a/server/src/main/java/org/elasticsearch/operator/service/OperatorUpdateErrorTask.java b/server/src/main/java/org/elasticsearch/operator/service/OperatorUpdateErrorTask.java index 1297505892e38..2a52b194c3612 100644 --- a/server/src/main/java/org/elasticsearch/operator/service/OperatorUpdateErrorTask.java +++ b/server/src/main/java/org/elasticsearch/operator/service/OperatorUpdateErrorTask.java @@ -25,10 +25,20 @@ * Cluster state update task that sets the error state of the operator metadata. * This is used when an operator cluster state update encounters error(s) while processing * the file. - * - * @param listener */ -public record OperatorUpdateErrorTask(ActionListener listener) implements ClusterStateTaskListener { +public class OperatorUpdateErrorTask implements ClusterStateTaskListener { + + private final OperatorClusterStateController.OperatorErrorState errorState; + private final ActionListener listener; + + public OperatorUpdateErrorTask( + OperatorClusterStateController.OperatorErrorState errorState, + ActionListener listener + ) { + this.errorState = errorState; + this.listener = listener; + } + private static final Logger logger = LogManager.getLogger(FileSettingsService.class); @Override @@ -36,39 +46,42 @@ public void onFailure(Exception e) { listener.onFailure(e); } + ActionListener listener() { + return listener; + } + + ClusterState execute(ClusterState currentState) { + ClusterState.Builder stateBuilder = new ClusterState.Builder(currentState); + Metadata.Builder metadataBuilder = Metadata.builder(currentState.metadata()); + OperatorMetadata operatorMetadata = currentState.metadata().operatorState(errorState.namespace()); + OperatorMetadata.Builder operatorMetadataBuilder = OperatorMetadata.builder(errorState.namespace(), operatorMetadata); + operatorMetadataBuilder.errorMetadata( + OperatorErrorMetadata.builder() + .version(errorState.version()) + .errorKind(errorState.errorKind()) + .errors(errorState.errors()) + .build() + ); + metadataBuilder.putOperatorState(operatorMetadataBuilder.build()); + ClusterState newState = stateBuilder.metadata(metadataBuilder).build(); + + return newState; + } + /** * Operator update cluster state task executor - * - * @param namespace of the state we are updating - * @param version of the update that failed - * @param errors the list of errors to report */ - public record OperatorUpdateErrorTaskExecutor( - String namespace, - Long version, - OperatorErrorMetadata.ErrorKind errorKind, - List errors - ) implements ClusterStateTaskExecutor { + public record OperatorUpdateErrorTaskExecutor() implements ClusterStateTaskExecutor { @Override public ClusterState execute(ClusterState currentState, List> taskContexts) throws Exception { for (final var taskContext : taskContexts) { + currentState = taskContext.getTask().execute(currentState); taskContext.success( () -> taskContext.getTask().listener().delegateFailure((l, s) -> l.onResponse(ActionResponse.Empty.INSTANCE)) ); } - - ClusterState.Builder stateBuilder = new ClusterState.Builder(currentState); - Metadata.Builder metadataBuilder = Metadata.builder(currentState.metadata()); - OperatorMetadata operatorMetadata = currentState.metadata().operatorState(namespace); - OperatorMetadata.Builder operatorMetadataBuilder = OperatorMetadata.builder(namespace, operatorMetadata); - operatorMetadataBuilder.errorMetadata( - OperatorErrorMetadata.builder().version(version).errorKind(errorKind).errors(errors).build() - ); - metadataBuilder.putOperatorState(operatorMetadataBuilder.build()); - ClusterState newState = stateBuilder.metadata(metadataBuilder).build(); - - return newState; + return currentState; } @Override diff --git a/server/src/main/java/org/elasticsearch/operator/service/OperatorUpdateStateTask.java b/server/src/main/java/org/elasticsearch/operator/service/OperatorUpdateStateTask.java index 49dcb1e3b6968..195106d91805a 100644 --- a/server/src/main/java/org/elasticsearch/operator/service/OperatorUpdateStateTask.java +++ b/server/src/main/java/org/elasticsearch/operator/service/OperatorUpdateStateTask.java @@ -15,42 +15,133 @@ import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.ClusterStateTaskExecutor; import org.elasticsearch.cluster.ClusterStateTaskListener; +import org.elasticsearch.cluster.metadata.Metadata; +import org.elasticsearch.cluster.metadata.OperatorErrorMetadata; +import org.elasticsearch.cluster.metadata.OperatorHandlerMetadata; +import org.elasticsearch.cluster.metadata.OperatorMetadata; import org.elasticsearch.cluster.routing.RerouteService; import org.elasticsearch.common.Priority; +import org.elasticsearch.operator.OperatorHandler; +import org.elasticsearch.operator.TransformState; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; + +import static org.elasticsearch.core.Strings.format; /** * Generic operator cluster state update task - * - * @param listener */ -public record OperatorUpdateStateTask(ActionListener listener) implements ClusterStateTaskListener { +public class OperatorUpdateStateTask implements ClusterStateTaskListener { private static final Logger logger = LogManager.getLogger(FileSettingsService.class); + private final String namespace; + private final OperatorClusterStateController.SettingsFile operatorStateFileContent; + private final Map> handlers; + private final Collection orderedHandlers; + private final Consumer recordErrorState; + private final ActionListener listener; + + public OperatorUpdateStateTask( + String namespace, + OperatorClusterStateController.SettingsFile operatorStateFileContent, + Map> handlers, + Collection orderedHandlers, + Consumer recordErrorState, + ActionListener listener + ) { + this.namespace = namespace; + this.operatorStateFileContent = operatorStateFileContent; + this.handlers = handlers; + this.orderedHandlers = orderedHandlers; + this.recordErrorState = recordErrorState; + this.listener = listener; + } + @Override public void onFailure(Exception e) { listener.onFailure(e); } + ActionListener listener() { + return listener; + } + + protected ClusterState execute(ClusterState state) { + OperatorMetadata existingMetadata = state.metadata().operatorState(namespace); + Map operatorState = operatorStateFileContent.state; + OperatorStateVersionMetadata stateVersionMetadata = operatorStateFileContent.metadata; + + OperatorMetadata.Builder operatorMetadataBuilder = new OperatorMetadata.Builder(namespace).version(stateVersionMetadata.version()); + List errors = new ArrayList<>(); + + for (var handlerKey : orderedHandlers) { + OperatorHandler handler = handlers.get(handlerKey); + try { + Set existingKeys = keysForHandler(existingMetadata, handlerKey); + TransformState transformState = handler.transform(operatorState.get(handlerKey), new TransformState(state, existingKeys)); + state = transformState.state(); + operatorMetadataBuilder.putHandler(new OperatorHandlerMetadata.Builder(handlerKey).keys(transformState.keys()).build()); + } catch (Exception e) { + errors.add(format("Error processing %s state change: %s", handler.name(), e.getMessage())); + } + } + + if (errors.isEmpty() == false) { + recordErrorState.accept( + new OperatorClusterStateController.OperatorErrorState( + namespace, + stateVersionMetadata.version(), + errors, + OperatorErrorMetadata.ErrorKind.VALIDATION + ) + ); + logger.error("Error processing state change request for [{}] with the following errors [{}]", namespace, errors); + + throw new IllegalStateException("Error processing state change request for " + namespace); + } + + // remove the last error if we had previously encountered any + operatorMetadataBuilder.errorMetadata(null); + + ClusterState.Builder stateBuilder = new ClusterState.Builder(state); + Metadata.Builder metadataBuilder = Metadata.builder(state.metadata()).putOperatorState(operatorMetadataBuilder.build()); + + return stateBuilder.metadata(metadataBuilder).build(); + } + + private Set keysForHandler(OperatorMetadata operatorMetadata, String handlerKey) { + if (operatorMetadata == null || operatorMetadata.handlers().get(handlerKey) == null) { + return Collections.emptySet(); + } + + return operatorMetadata.handlers().get(handlerKey).keys(); + } + /** * Operator update cluster state task executor * * @param namespace of the state we are updating * @param rerouteService instance of RerouteService so we can execute reroute after cluster state is published */ - public record OperatorUpdateStateTaskExecutor(String namespace, ClusterState newState, RerouteService rerouteService) + public record OperatorUpdateStateTaskExecutor(String namespace, RerouteService rerouteService) implements ClusterStateTaskExecutor { @Override public ClusterState execute(ClusterState currentState, List> taskContexts) throws Exception { for (final var taskContext : taskContexts) { + currentState = taskContext.getTask().execute(currentState); taskContext.success( () -> taskContext.getTask().listener().delegateFailure((l, s) -> l.onResponse(ActionResponse.Empty.INSTANCE)) ); } - return newState; + return currentState; } @Override diff --git a/server/src/test/java/org/elasticsearch/action/admin/cluster/settings/ClusterUpdateSettingsRequestTests.java b/server/src/test/java/org/elasticsearch/action/admin/cluster/settings/ClusterUpdateSettingsRequestTests.java index 4fd92c1c32268..ce77b451e5dc2 100644 --- a/server/src/test/java/org/elasticsearch/action/admin/cluster/settings/ClusterUpdateSettingsRequestTests.java +++ b/server/src/test/java/org/elasticsearch/action/admin/cluster/settings/ClusterUpdateSettingsRequestTests.java @@ -94,7 +94,7 @@ public void testOperatorHandler() throws IOException { clusterSettings ); - assertEquals(OperatorClusterUpdateSettingsAction.KEY, action.operatorHandlerName().get()); + assertEquals(OperatorClusterUpdateSettingsAction.NAME, action.operatorHandlerName().get()); String oneSettingJSON = """ { diff --git a/server/src/test/java/org/elasticsearch/action/support/master/TransportMasterNodeActionTests.java b/server/src/test/java/org/elasticsearch/action/support/master/TransportMasterNodeActionTests.java index 47dd735e7cebd..e27c9aae3180e 100644 --- a/server/src/test/java/org/elasticsearch/action/support/master/TransportMasterNodeActionTests.java +++ b/server/src/test/java/org/elasticsearch/action/support/master/TransportMasterNodeActionTests.java @@ -301,7 +301,7 @@ protected ClusterBlockException checkBlock(ClusterUpdateSettingsRequest request, @Override protected Optional operatorHandlerName() { - return Optional.of(OperatorClusterUpdateSettingsAction.KEY); + return Optional.of(OperatorClusterUpdateSettingsAction.NAME); } @Override @@ -754,9 +754,9 @@ protected ClusterBlockException checkBlock(Request request, ClusterState state) } public void testRejectOperatorConflictClusterStateUpdate() { - OperatorHandlerMetadata hmOne = new OperatorHandlerMetadata.Builder(OperatorClusterUpdateSettingsAction.KEY).keys(Set.of("a", "b")) + OperatorHandlerMetadata hmOne = new OperatorHandlerMetadata.Builder(OperatorClusterUpdateSettingsAction.NAME).keys(Set.of("a", "b")) .build(); - OperatorHandlerMetadata hmThree = new OperatorHandlerMetadata.Builder(OperatorClusterUpdateSettingsAction.KEY).keys( + OperatorHandlerMetadata hmThree = new OperatorHandlerMetadata.Builder(OperatorClusterUpdateSettingsAction.NAME).keys( Set.of("e", "f") ).build(); diff --git a/server/src/test/java/org/elasticsearch/operator/service/OperatorClusterStateControllerTests.java b/server/src/test/java/org/elasticsearch/operator/service/OperatorClusterStateControllerTests.java index 200b9a7bc4e18..63023b2a6ab7c 100644 --- a/server/src/test/java/org/elasticsearch/operator/service/OperatorClusterStateControllerTests.java +++ b/server/src/test/java/org/elasticsearch/operator/service/OperatorClusterStateControllerTests.java @@ -11,6 +11,7 @@ import org.elasticsearch.Version; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.ActionResponse; +import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsRequest; import org.elasticsearch.cluster.ClusterName; import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.ClusterStateAckListener; @@ -42,7 +43,9 @@ import static org.hamcrest.Matchers.is; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -118,19 +121,29 @@ public void testUpdateStateTasks() throws Exception { OperatorUpdateStateTask.OperatorUpdateStateTaskExecutor taskExecutor = new OperatorUpdateStateTask.OperatorUpdateStateTaskExecutor( "test", - state, clusterService.getRerouteService() ); AtomicBoolean successCalled = new AtomicBoolean(false); - OperatorUpdateStateTask task = new OperatorUpdateStateTask(new ActionListener<>() { - @Override - public void onResponse(ActionResponse.Empty empty) {} + OperatorUpdateStateTask task = spy( + new OperatorUpdateStateTask( + "test", + null, + Collections.emptyMap(), + Collections.emptySet(), + (errorState) -> {}, + new ActionListener<>() { + @Override + public void onResponse(ActionResponse.Empty empty) {} + + @Override + public void onFailure(Exception e) {} + } + ) + ); - @Override - public void onFailure(Exception e) {} - }); + doReturn(state).when(task).execute(any()); ClusterStateTaskExecutor.TaskContext taskContext = new ClusterStateTaskExecutor.TaskContext<>() { @Override @@ -160,6 +173,7 @@ public void onFailure(Exception failure) {} ClusterState newState = taskExecutor.execute(state, List.of(taskContext)); assertEquals(state, newState); assertTrue(successCalled.get()); + verify(task, times(1)).execute(any()); taskExecutor.clusterStatePublished(state); verify(rerouteService, times(1)).reroute(anyString(), any(), any()); @@ -168,14 +182,54 @@ public void onFailure(Exception failure) {} public void testErrorStateTask() throws Exception { ClusterState state = ClusterState.builder(new ClusterName("test")).build(); - OperatorUpdateErrorTask.OperatorUpdateErrorTaskExecutor executor = new OperatorUpdateErrorTask.OperatorUpdateErrorTaskExecutor( - "test", - 1L, - OperatorErrorMetadata.ErrorKind.PARSING, - List.of("some parse error", "some io error") + OperatorUpdateErrorTask task = spy( + new OperatorUpdateErrorTask( + new OperatorClusterStateController.OperatorErrorState( + "test", + 1L, + List.of("some parse error", "some io error"), + OperatorErrorMetadata.ErrorKind.PARSING + ), + new ActionListener<>() { + @Override + public void onResponse(ActionResponse.Empty empty) {} + + @Override + public void onFailure(Exception e) {} + } + ) ); - ClusterState newState = executor.execute(state, Collections.emptyList()); + OperatorUpdateErrorTask.OperatorUpdateErrorTaskExecutor.TaskContext taskContext = + new OperatorUpdateErrorTask.OperatorUpdateErrorTaskExecutor.TaskContext<>() { + @Override + public OperatorUpdateErrorTask getTask() { + return task; + } + + @Override + public void success(Runnable onPublicationSuccess) { + onPublicationSuccess.run(); + } + + @Override + public void success(Consumer publishedStateConsumer) {} + + @Override + public void success(Runnable onPublicationSuccess, ClusterStateAckListener clusterStateAckListener) {} + + @Override + public void success(Consumer publishedStateConsumer, ClusterStateAckListener clusterStateAckListener) {} + + @Override + public void onFailure(Exception failure) {} + }; + + OperatorUpdateErrorTask.OperatorUpdateErrorTaskExecutor executor = new OperatorUpdateErrorTask.OperatorUpdateErrorTaskExecutor(); + + ClusterState newState = executor.execute(state, List.of(taskContext)); + + verify(task, times(1)).execute(any()); OperatorMetadata operatorMetadata = newState.metadata().operatorState("test"); assertNotNull(operatorMetadata); @@ -207,7 +261,7 @@ public void testcheckMetadataVersion() { public void testHandlerOrdering() { OperatorHandler oh1 = new OperatorHandler<>() { @Override - public String key() { + public String name() { return "one"; } @@ -224,7 +278,7 @@ public Collection dependencies() { OperatorHandler oh2 = new OperatorHandler<>() { @Override - public String key() { + public String name() { return "two"; } @@ -236,7 +290,7 @@ public TransformState transform(Object source, TransformState prevState) throws OperatorHandler oh3 = new OperatorHandler<>() { @Override - public String key() { + public String name() { return "three"; } @@ -274,7 +328,7 @@ public Collection dependencies() { // Change the second handler so that we create cycle oh2 = new OperatorHandler<>() { @Override - public String key() { + public String name() { return "two"; } @@ -298,4 +352,35 @@ public Collection dependencies() { ) ); } + + public void testDuplicateHandlerNames() { + ClusterSettings clusterSettings = new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS); + ClusterService clusterService = mock(ClusterService.class); + final ClusterName clusterName = new ClusterName("elasticsearch"); + + ClusterState state = ClusterState.builder(clusterName).build(); + when(clusterService.state()).thenReturn(state); + + OperatorClusterStateController controller = new OperatorClusterStateController(clusterService); + + assertTrue( + expectThrows( + IllegalStateException.class, + () -> controller.initHandlers(List.of(new OperatorClusterUpdateSettingsAction(clusterSettings), new TestHandler())) + ).getMessage().startsWith("Duplicate key cluster_settings") + ); + } + + class TestHandler implements OperatorHandler { + + @Override + public String name() { + return OperatorClusterUpdateSettingsAction.NAME; + } + + @Override + public TransformState transform(Object source, TransformState prevState) throws Exception { + return prevState; + } + } } diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportDeleteLifecycleAction.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportDeleteLifecycleAction.java index ba8ab6217e8b6..09e4faebf0710 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportDeleteLifecycleAction.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportDeleteLifecycleAction.java @@ -117,7 +117,7 @@ protected ClusterBlockException checkBlock(Request request, ClusterState state) @Override protected Optional operatorHandlerName() { - return Optional.of(OperatorLifecycleAction.KEY); + return Optional.of(OperatorLifecycleAction.NAME); } @Override diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportPutLifecycleAction.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportPutLifecycleAction.java index bc6df5c7696b5..dd40d99798c77 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportPutLifecycleAction.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportPutLifecycleAction.java @@ -301,7 +301,7 @@ protected ClusterBlockException checkBlock(Request request, ClusterState state) @Override protected Optional operatorHandlerName() { - return Optional.of(OperatorLifecycleAction.KEY); + return Optional.of(OperatorLifecycleAction.NAME); } @Override diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/operator/OperatorLifecycleAction.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/operator/OperatorLifecycleAction.java index 7b7c15f249ade..3beb80e1f8124 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/operator/OperatorLifecycleAction.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/operator/OperatorLifecycleAction.java @@ -36,7 +36,7 @@ public class OperatorLifecycleAction implements OperatorHandler Date: Wed, 8 Jun 2022 20:29:45 -0400 Subject: [PATCH 40/48] Make metadata hashing consistent --- .../cluster/metadata/OperatorHandlerMetadata.java | 3 ++- .../cluster/metadata/OperatorMetadata.java | 6 ++++-- .../cluster/metadata/ToAndFromJsonMetadataTests.java | 12 ++++++------ 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/OperatorHandlerMetadata.java b/server/src/main/java/org/elasticsearch/cluster/metadata/OperatorHandlerMetadata.java index 5a8947d03a7cd..5792f075dd5e7 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/OperatorHandlerMetadata.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/OperatorHandlerMetadata.java @@ -21,6 +21,7 @@ import java.util.Collections; import java.util.HashSet; import java.util.Set; +import java.util.TreeSet; /** * Metadata class to hold the operator set keys for each operator handler @@ -81,7 +82,7 @@ public OperatorHandlerMetadata build() { public static void toXContent(OperatorHandlerMetadata metadata, XContentBuilder builder, ToXContent.Params params) throws IOException { builder.startObject(metadata.name()); - builder.stringListField(HANDLER_KEYS, metadata.keys); + builder.stringListField(HANDLER_KEYS, new TreeSet<>(metadata.keys)); builder.endObject(); } diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/OperatorMetadata.java b/server/src/main/java/org/elasticsearch/cluster/metadata/OperatorMetadata.java index 78f55e9a426d3..c87ece109e048 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/OperatorMetadata.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/OperatorMetadata.java @@ -26,6 +26,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.TreeSet; /** * Metadata class that contains information about cluster settings/entities set @@ -184,8 +185,9 @@ public static void toXContent(OperatorMetadata operatorMetadata, XContentBuilder builder.startObject(operatorMetadata.namespace()); builder.field(VERSION, operatorMetadata.version()); builder.startObject(HANDLERS); - for (OperatorHandlerMetadata handlerMetadata : operatorMetadata.handlers().values()) { - OperatorHandlerMetadata.Builder.toXContent(handlerMetadata, builder, params); + var sortedKeys = new TreeSet<>(operatorMetadata.handlers.keySet()); + for (var key : sortedKeys) { + OperatorHandlerMetadata.Builder.toXContent(operatorMetadata.handlers.get(key), builder, params); } builder.endObject(); builder.field(ERRORS_METADATA, operatorMetadata.errorMetadata); diff --git a/server/src/test/java/org/elasticsearch/cluster/metadata/ToAndFromJsonMetadataTests.java b/server/src/test/java/org/elasticsearch/cluster/metadata/ToAndFromJsonMetadataTests.java index c85d9bd5c2cae..7bd30d38190f3 100644 --- a/server/src/test/java/org/elasticsearch/cluster/metadata/ToAndFromJsonMetadataTests.java +++ b/server/src/test/java/org/elasticsearch/cluster/metadata/ToAndFromJsonMetadataTests.java @@ -779,14 +779,14 @@ public void testToXContentAPIOperatorMetadata() throws IOException { "handlers" : { "one" : { "keys" : [ - "b", - "a" + "a", + "b" ] }, "two" : { "keys" : [ - "d", - "c" + "c", + "d" ] } }, @@ -804,8 +804,8 @@ public void testToXContentAPIOperatorMetadata() throws IOException { "handlers" : { "three" : { "keys" : [ - "f", - "e" + "e", + "f" ] } }, From c89dcccff6a3fec81540c22d4020fa384ecadafc Mon Sep 17 00:00:00 2001 From: Nikola Grcevski Date: Thu, 9 Jun 2022 11:25:30 -0400 Subject: [PATCH 41/48] Support async error reporting in the controller --- .../operator/service/FileSettingsService.java | 17 +++++- .../OperatorClusterStateController.java | 58 ++++++++++++++----- .../OperatorClusterStateControllerTests.java | 40 ++++++++++--- .../operator/OperatorILMControllerTests.java | 17 ++++-- 4 files changed, 103 insertions(+), 29 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/operator/service/FileSettingsService.java b/server/src/main/java/org/elasticsearch/operator/service/FileSettingsService.java index bab17d35400d6..263bb1cf3aca6 100644 --- a/server/src/main/java/org/elasticsearch/operator/service/FileSettingsService.java +++ b/server/src/main/java/org/elasticsearch/operator/service/FileSettingsService.java @@ -196,7 +196,7 @@ synchronized void startWatcher() { if (watchedFileChanged(path)) { processFileSettings(path); } - } catch (IOException e) { + } catch (Exception e) { logger.warn("unable to watch or read operator settings file", e); } } else { @@ -205,7 +205,10 @@ synchronized void startWatcher() { } } } catch (Exception e) { - logger.debug("encountered exception watching, shutting down watcher thread.", e); + if (logger.isDebugEnabled()) { + logger.debug("encountered exception watching", e); + } + logger.info("shutting down watcher thread"); } finally { watcherThreadLatch.countDown(); } @@ -243,7 +246,15 @@ void processFileSettings(Path path) { try ( XContentParser parser = XContentType.JSON.xContent().createParser(XContentParserConfiguration.EMPTY, Files.newInputStream(path)) ) { - controller.process(NAMESPACE, parser); + controller.process(NAMESPACE, parser, (e) -> { + if (e != null) { + if (e instanceof OperatorClusterStateController.IncompatibleVersionException) { + logger.info(e.getMessage()); + } else { + logger.error("Error processing operator settings json file", e); + } + } + }); } catch (Exception e) { logger.error("Error processing operator settings json file", e); } diff --git a/server/src/main/java/org/elasticsearch/operator/service/OperatorClusterStateController.java b/server/src/main/java/org/elasticsearch/operator/service/OperatorClusterStateController.java index f069517e972d8..d3a10158d5f26 100644 --- a/server/src/main/java/org/elasticsearch/operator/service/OperatorClusterStateController.java +++ b/server/src/main/java/org/elasticsearch/operator/service/OperatorClusterStateController.java @@ -28,9 +28,12 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.Consumer; import java.util.function.Function; import java.util.stream.Collectors; +import static org.elasticsearch.core.Strings.format; + /** * Controller class for applying file based settings to ClusterState. * This class contains the logic about validation, ordering and applying of @@ -85,9 +88,11 @@ static class SettingsFile { * * @param namespace the namespace under which we'll store the operator keys in the cluster state metadata * @param parser the XContentParser to process - * @throws IllegalStateException if the content has errors and the cluster state cannot be correctly applied + * @param errorListener a consumer called with IllegalStateException if the content has errors and the + * cluster state cannot be correctly applied, IncompatibleVersionException if the content is stale or + * incompatible with this node {@link Version}, null if successful. */ - public void process(String namespace, XContentParser parser) { + public void process(String namespace, XContentParser parser, Consumer errorListener) { SettingsFile operatorStateFileContent; try { @@ -97,7 +102,8 @@ public void process(String namespace, XContentParser parser) { recordErrorState(new OperatorErrorState(namespace, -1L, errors, OperatorErrorMetadata.ErrorKind.PARSING)); logger.error("Error processing state change request for [{}] with the following errors [{}]", namespace, errors); - throw new IllegalStateException("Error processing state change request for " + namespace, e); + errorListener.accept(new IllegalStateException("Error processing state change request for " + namespace, e)); + return; } Map operatorState = operatorStateFileContent.state; @@ -113,12 +119,13 @@ public void process(String namespace, XContentParser parser) { ); logger.error("Error processing state change request for [{}] with the following errors [{}]", namespace, errors); - throw new IllegalStateException("Error processing state change request for " + namespace, e); + errorListener.accept(new IllegalStateException("Error processing state change request for " + namespace, e)); + return; } ClusterState state = clusterService.state(); OperatorMetadata existingMetadata = state.metadata().operatorState(namespace); - if (checkMetadataVersion(existingMetadata, stateVersionMetadata) == false) { + if (checkMetadataVersion(existingMetadata, stateVersionMetadata, errorListener) == false) { return; } @@ -135,11 +142,13 @@ public void process(String namespace, XContentParser parser) { @Override public void onResponse(ActionResponse.Empty empty) { logger.info("Successfully applied new cluster state for namespace [{}]", namespace); + errorListener.accept(null); } @Override public void onFailure(Exception e) { logger.error("Failed to apply operator cluster state", e); + errorListener.accept(e); } } ), @@ -149,20 +158,32 @@ public void onFailure(Exception e) { } // package private for testing - static boolean checkMetadataVersion(OperatorMetadata existingMetadata, OperatorStateVersionMetadata stateVersionMetadata) { + static boolean checkMetadataVersion( + OperatorMetadata existingMetadata, + OperatorStateVersionMetadata stateVersionMetadata, + Consumer errorListener + ) { if (Version.CURRENT.before(stateVersionMetadata.minCompatibleVersion())) { - logger.info( - "Cluster state version [{}] is not compatible with this Elasticsearch node", - stateVersionMetadata.minCompatibleVersion() + errorListener.accept( + new IncompatibleVersionException( + format( + "Cluster state version [%s] is not compatible with this Elasticsearch node", + stateVersionMetadata.minCompatibleVersion() + ) + ) ); return false; } if (existingMetadata != null && existingMetadata.version() >= stateVersionMetadata.version()) { - logger.info( - "Not updating cluster state because version [{}] is less or equal to the current metadata version [{}]", - stateVersionMetadata.version(), - existingMetadata.version() + errorListener.accept( + new IncompatibleVersionException( + format( + "Not updating cluster state because version [%s] is less or equal to the current metadata version [%s]", + stateVersionMetadata.version(), + existingMetadata.version() + ) + ) ); return false; } @@ -236,4 +257,15 @@ private void addStateHandler(String key, Set keys, LinkedHashSet visited.remove(key); ordered.add(key); } + + /** + * {@link IncompatibleVersionException} is thrown when we try to update the cluster state + * without changing the update version id, or if we try to update cluster state on + * an incompatible Elasticsearch version in mixed cluster mode. + */ + public static class IncompatibleVersionException extends RuntimeException { + public IncompatibleVersionException(String message) { + super(message); + } + } } diff --git a/server/src/test/java/org/elasticsearch/operator/service/OperatorClusterStateControllerTests.java b/server/src/test/java/org/elasticsearch/operator/service/OperatorClusterStateControllerTests.java index 63023b2a6ab7c..744f5fafe49e0 100644 --- a/server/src/test/java/org/elasticsearch/operator/service/OperatorClusterStateControllerTests.java +++ b/server/src/test/java/org/elasticsearch/operator/service/OperatorClusterStateControllerTests.java @@ -36,6 +36,7 @@ import java.util.List; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import static org.hamcrest.Matchers.anyOf; @@ -77,11 +78,13 @@ public void testOperatorController() throws IOException { } """; + AtomicReference x = new AtomicReference<>(); + try (XContentParser parser = XContentType.JSON.xContent().createParser(XContentParserConfiguration.EMPTY, testJSON)) { - assertEquals( - "Error processing state change request for operator", - expectThrows(IllegalStateException.class, () -> controller.process("operator", parser)).getMessage() - ); + controller.process("operator", parser, (e) -> x.set(e)); + + assertTrue(x.get() instanceof IllegalStateException); + assertEquals("Error processing state change request for operator", x.get().getMessage()); } testJSON = """ @@ -108,7 +111,11 @@ public void testOperatorController() throws IOException { """; try (XContentParser parser = XContentType.JSON.xContent().createParser(XContentParserConfiguration.EMPTY, testJSON)) { - controller.process("operator", parser); + controller.process("operator", parser, (e) -> { + if (e != null) { + fail("Should not fail"); + } + }); } } @@ -243,19 +250,36 @@ public void testcheckMetadataVersion() { OperatorMetadata operatorMetadata = OperatorMetadata.builder("test").version(123L).build(); assertTrue( - OperatorClusterStateController.checkMetadataVersion(operatorMetadata, new OperatorStateVersionMetadata(124L, Version.CURRENT)) + OperatorClusterStateController.checkMetadataVersion( + operatorMetadata, + new OperatorStateVersionMetadata(124L, Version.CURRENT), + (e) -> {} + ) ); + AtomicReference x = new AtomicReference<>(); + assertFalse( - OperatorClusterStateController.checkMetadataVersion(operatorMetadata, new OperatorStateVersionMetadata(123L, Version.CURRENT)) + OperatorClusterStateController.checkMetadataVersion( + operatorMetadata, + new OperatorStateVersionMetadata(123L, Version.CURRENT), + (e) -> x.set(e) + ) ); + assertTrue(x.get() instanceof OperatorClusterStateController.IncompatibleVersionException); + assertTrue(x.get().getMessage().contains("is less or equal to the current metadata version")); + assertFalse( OperatorClusterStateController.checkMetadataVersion( operatorMetadata, - new OperatorStateVersionMetadata(124L, Version.fromId(Version.CURRENT.id + 1)) + new OperatorStateVersionMetadata(124L, Version.fromId(Version.CURRENT.id + 1)), + (e) -> x.set(e) ) ); + + assertEquals(OperatorClusterStateController.IncompatibleVersionException.class, x.get().getClass()); + assertTrue(x.get().getMessage().contains("is not compatible with this Elasticsearch node")); } public void testHandlerOrdering() { diff --git a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/action/operator/OperatorILMControllerTests.java b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/action/operator/OperatorILMControllerTests.java index cc88dbfcd9932..ad6624740fd44 100644 --- a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/action/operator/OperatorILMControllerTests.java +++ b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/action/operator/OperatorILMControllerTests.java @@ -48,6 +48,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.concurrent.atomic.AtomicReference; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.mockito.Mockito.mock; @@ -267,11 +268,13 @@ public void testOperatorController() throws IOException { } }"""; + AtomicReference x = new AtomicReference<>(); + try (XContentParser parser = XContentType.JSON.xContent().createParser(XContentParserConfiguration.EMPTY, testJSON)) { - assertEquals( - "Error processing state change request for operator", - expectThrows(IllegalStateException.class, () -> controller.process("operator", parser)).getMessage() - ); + controller.process("operator", parser, (e) -> x.set(e)); + + assertTrue(x.get() instanceof IllegalStateException); + assertEquals("Error processing state change request for operator", x.get().getMessage()); } Client client = mock(Client.class); @@ -287,7 +290,11 @@ public void testOperatorController() throws IOException { ); try (XContentParser parser = XContentType.JSON.xContent().createParser(XContentParserConfiguration.EMPTY, testJSON)) { - controller.process("operator", parser); + controller.process("operator", parser, (e) -> { + if (e != null) { + fail("Should not fail"); + } + }); } } } From e4b5938fc83430ad21b3f5b78d016f997315d931 Mon Sep 17 00:00:00 2001 From: Nikola Grcevski Date: Mon, 13 Jun 2022 13:42:49 -0400 Subject: [PATCH 42/48] Apply martijnvg's suggestions --- .../operator/OperatorHandler.java | 6 ++--- .../service/OperatorUpdateStateTask.java | 4 +-- .../action/TransportPutLifecycleAction.java | 27 ++++++++++++++----- .../operator/OperatorLifecycleAction.java | 5 +++- 4 files changed, 28 insertions(+), 14 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/operator/OperatorHandler.java b/server/src/main/java/org/elasticsearch/operator/OperatorHandler.java index 61ee998541c13..553fc60187a68 100644 --- a/server/src/main/java/org/elasticsearch/operator/OperatorHandler.java +++ b/server/src/main/java/org/elasticsearch/operator/OperatorHandler.java @@ -103,14 +103,14 @@ default void validate(T request) { * Convenience method that creates a {@link XContentParser} from a content map so that it can be passed to * existing REST based code for input parsing. * + * @config XContentParserConfiguration for this mapper * @param source the operator content as a map * @return */ - default XContentParser mapToXContentParser(Map source) { + default XContentParser mapToXContentParser(XContentParserConfiguration config, Map source) { try (XContentBuilder builder = XContentFactory.contentBuilder(XContentType.JSON)) { builder.map(source); - return XContentFactory.xContent(builder.contentType()) - .createParser(XContentParserConfiguration.EMPTY, Strings.toString(builder)); + return XContentFactory.xContent(builder.contentType()).createParser(config, Strings.toString(builder)); } catch (IOException e) { throw new ElasticsearchGenerationException("Failed to generate [" + source + "]", e); } diff --git a/server/src/main/java/org/elasticsearch/operator/service/OperatorUpdateStateTask.java b/server/src/main/java/org/elasticsearch/operator/service/OperatorUpdateStateTask.java index 195106d91805a..8d2a141012db8 100644 --- a/server/src/main/java/org/elasticsearch/operator/service/OperatorUpdateStateTask.java +++ b/server/src/main/java/org/elasticsearch/operator/service/OperatorUpdateStateTask.java @@ -137,9 +137,7 @@ public record OperatorUpdateStateTaskExecutor(String namespace, RerouteService r public ClusterState execute(ClusterState currentState, List> taskContexts) throws Exception { for (final var taskContext : taskContexts) { currentState = taskContext.getTask().execute(currentState); - taskContext.success( - () -> taskContext.getTask().listener().delegateFailure((l, s) -> l.onResponse(ActionResponse.Empty.INSTANCE)) - ); + taskContext.success(() -> taskContext.getTask().listener().onResponse(ActionResponse.Empty.INSTANCE)); } return currentState; } diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportPutLifecycleAction.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportPutLifecycleAction.java index dd40d99798c77..071d7cffe228a 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportPutLifecycleAction.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportPutLifecycleAction.java @@ -115,7 +115,7 @@ protected void masterOperation(Task task, Request request, ClusterState state, A submitUnbatchedTask( "put-lifecycle-" + request.getPolicy().getName(), - new UpdateLifecyclePolicyTask(request, listener, licenseState, filteredHeaders, xContentRegistry, client) + new UpdateLifecyclePolicyTask(request, listener, licenseState, filteredHeaders, xContentRegistry, client, true) ); } @@ -125,6 +125,7 @@ public static class UpdateLifecyclePolicyTask extends AckedClusterStateUpdateTas private final Map filteredHeaders; private final NamedXContentRegistry xContentRegistry; private final Client client; + private final boolean verboseLogging; public UpdateLifecyclePolicyTask( Request request, @@ -132,7 +133,8 @@ public UpdateLifecyclePolicyTask( XPackLicenseState licenseState, Map filteredHeaders, NamedXContentRegistry xContentRegistry, - Client client + Client client, + boolean verboseLogging ) { super(request, listener); this.request = request; @@ -140,15 +142,24 @@ public UpdateLifecyclePolicyTask( this.filteredHeaders = filteredHeaders; this.xContentRegistry = xContentRegistry; this.client = client; + this.verboseLogging = verboseLogging; } + /** + * Constructor used in operator mode. It disables verbose logging and has no filtered headers. + * + * @param request + * @param licenseState + * @param xContentRegistry + * @param client + */ public UpdateLifecyclePolicyTask( Request request, XPackLicenseState licenseState, NamedXContentRegistry xContentRegistry, Client client ) { - this(request, null, licenseState, new HashMap<>(), xContentRegistry, client); + this(request, null, licenseState, new HashMap<>(), xContentRegistry, client, false); } @Override @@ -174,10 +185,12 @@ public ClusterState execute(ClusterState currentState) throws Exception { Instant.now().toEpochMilli() ); LifecyclePolicyMetadata oldPolicy = newPolicies.put(lifecyclePolicyMetadata.getName(), lifecyclePolicyMetadata); - if (oldPolicy == null) { - logger.info("adding index lifecycle policy [{}]", request.getPolicy().getName()); - } else { - logger.info("updating index lifecycle policy [{}]", request.getPolicy().getName()); + if (verboseLogging) { + if (oldPolicy == null) { + logger.info("adding index lifecycle policy [{}]", request.getPolicy().getName()); + } else { + logger.info("updating index lifecycle policy [{}]", request.getPolicy().getName()); + } } IndexLifecycleMetadata newMetadata = new IndexLifecycleMetadata(newPolicies, currentMetadata.getOperationMode()); stateBuilder.metadata(Metadata.builder(currentState.getMetadata()).putCustom(IndexLifecycleMetadata.TYPE, newMetadata).build()); diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/operator/OperatorLifecycleAction.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/operator/OperatorLifecycleAction.java index 3beb80e1f8124..ceec783d775d6 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/operator/OperatorLifecycleAction.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/operator/OperatorLifecycleAction.java @@ -14,7 +14,9 @@ import org.elasticsearch.operator.TransformState; import org.elasticsearch.xcontent.NamedXContentRegistry; import org.elasticsearch.xcontent.XContentParser; +import org.elasticsearch.xcontent.XContentParserConfiguration; import org.elasticsearch.xpack.core.ilm.action.PutLifecycleAction; +import org.elasticsearch.xpack.core.template.LifecyclePolicyConfig; import org.elasticsearch.xpack.ilm.action.TransportDeleteLifecycleAction; import org.elasticsearch.xpack.ilm.action.TransportPutLifecycleAction; @@ -57,7 +59,8 @@ public Collection prepare(Object input) throws IOExc for (String name : source.keySet()) { Map content = (Map) source.get(name); - try (XContentParser parser = mapToXContentParser(content)) { + var config = XContentParserConfiguration.EMPTY.withRegistry(LifecyclePolicyConfig.DEFAULT_X_CONTENT_REGISTRY); + try (XContentParser parser = mapToXContentParser(config, content)) { PutLifecycleAction.Request request = PutLifecycleAction.Request.parseRequest(name, parser); validate(request); result.add(request); From 5630b52f3f209f187c70121de155450b9d9dcab4 Mon Sep 17 00:00:00 2001 From: Nikola Grcevski Date: Mon, 13 Jun 2022 20:34:57 -0400 Subject: [PATCH 43/48] Replace registration with SPI --- .../elasticsearch/action/ActionModule.java | 15 ++++++--- .../java/org/elasticsearch/node/Node.java | 2 +- .../operator/OperatorHandlerProvider.java | 27 ++++++++++++++++ .../elasticsearch/plugins/ActionPlugin.java | 5 --- .../elasticsearch/plugins/PluginsService.java | 16 +++++++++- .../plugin/ilm/src/main/java/module-info.java | 22 +++++++++++++ .../xpack/ilm/IndexLifecycle.java | 11 +++---- .../TransportDeleteLifecycleAction.java | 2 +- .../action/TransportPutLifecycleAction.java | 2 +- .../operator/ILMOperatorHandlerProvider.java | 32 +++++++++++++++++++ .../action}/OperatorLifecycleAction.java | 2 +- ...ticsearch.operator.OperatorHandlerProvider | 1 + .../TransportDeleteLifecycleActionTests.java | 2 +- .../TransportPutLifecycleActionTests.java | 2 +- .../operator/OperatorILMControllerTests.java | 1 + 15 files changed, 119 insertions(+), 23 deletions(-) create mode 100644 server/src/main/java/org/elasticsearch/operator/OperatorHandlerProvider.java create mode 100644 x-pack/plugin/ilm/src/main/java/module-info.java create mode 100644 x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/operator/ILMOperatorHandlerProvider.java rename x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/{action/operator => operator/action}/OperatorLifecycleAction.java (98%) create mode 100644 x-pack/plugin/ilm/src/main/resources/META-INF/services/org.elasticsearch.operator.OperatorHandlerProvider diff --git a/server/src/main/java/org/elasticsearch/action/ActionModule.java b/server/src/main/java/org/elasticsearch/action/ActionModule.java index c55bd150f8ca9..624575bef0ab1 100644 --- a/server/src/main/java/org/elasticsearch/action/ActionModule.java +++ b/server/src/main/java/org/elasticsearch/action/ActionModule.java @@ -267,6 +267,7 @@ import org.elasticsearch.indices.breaker.CircuitBreakerService; import org.elasticsearch.indices.store.TransportNodesListShardStoreMetadata; import org.elasticsearch.operator.OperatorHandler; +import org.elasticsearch.operator.OperatorHandlerProvider; import org.elasticsearch.operator.action.OperatorClusterUpdateSettingsAction; import org.elasticsearch.operator.service.OperatorClusterStateController; import org.elasticsearch.persistent.CompletionPersistentTaskAction; @@ -275,6 +276,7 @@ import org.elasticsearch.persistent.UpdatePersistentTaskStatusAction; import org.elasticsearch.plugins.ActionPlugin; import org.elasticsearch.plugins.ActionPlugin.ActionHandler; +import org.elasticsearch.plugins.PluginsService; import org.elasticsearch.plugins.interceptor.RestInterceptorActionPlugin; import org.elasticsearch.rest.RestController; import org.elasticsearch.rest.RestHandler; @@ -891,13 +893,18 @@ public void initRestHandlers(Supplier nodesInCluster) { registerHandler.accept(new RestCatAction(catActions)); } - public void initOperatorHandlers() { + /** + * Initializes the operator action handlers for Elasticsearch and it's modules/plugins + * + * @param pluginsService needed to load all modules/plugins operator handlers through SPI + */ + public void initOperatorHandlers(PluginsService pluginsService) { List> handlers = new ArrayList<>(); + List pluginHandlers = pluginsService.loadServiceProviders(OperatorHandlerProvider.class); + handlers.add(new OperatorClusterUpdateSettingsAction(clusterSettings)); - for (ActionPlugin plugin : actionPlugins) { - handlers.addAll(plugin.getOperatorHandlers()); - } + pluginHandlers.forEach(h -> handlers.addAll(h.handlers())); operatorController.initHandlers(handlers); } diff --git a/server/src/main/java/org/elasticsearch/node/Node.java b/server/src/main/java/org/elasticsearch/node/Node.java index 2395359e859c7..910325cb324b9 100644 --- a/server/src/main/java/org/elasticsearch/node/Node.java +++ b/server/src/main/java/org/elasticsearch/node/Node.java @@ -1024,7 +1024,7 @@ protected Node( logger.debug("initializing HTTP handlers ..."); actionModule.initRestHandlers(() -> clusterService.state().nodesIfRecovered()); logger.debug("initializing operator handlers ..."); - actionModule.initOperatorHandlers(); + actionModule.initOperatorHandlers(pluginsService); logger.info("initialized"); success = true; diff --git a/server/src/main/java/org/elasticsearch/operator/OperatorHandlerProvider.java b/server/src/main/java/org/elasticsearch/operator/OperatorHandlerProvider.java new file mode 100644 index 0000000000000..3446a60797e68 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/operator/OperatorHandlerProvider.java @@ -0,0 +1,27 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.operator; + +import org.elasticsearch.action.support.master.MasterNodeRequest; + +import java.util.Collection; + +/** + * SPI service interface for supplying OperatorHandler implementations to Elasticsearch + * from plugins/modules. + */ +public interface OperatorHandlerProvider { + /** + * Returns a list of OperatorHandler implementations that a module/plugin supplies. + * @see OperatorHandler + * + * @return a list of ${@link OperatorHandler}s + */ + Collection>> handlers(); +} diff --git a/server/src/main/java/org/elasticsearch/plugins/ActionPlugin.java b/server/src/main/java/org/elasticsearch/plugins/ActionPlugin.java index de324e864e869..207920cd60ac9 100644 --- a/server/src/main/java/org/elasticsearch/plugins/ActionPlugin.java +++ b/server/src/main/java/org/elasticsearch/plugins/ActionPlugin.java @@ -22,7 +22,6 @@ import org.elasticsearch.common.settings.IndexScopedSettings; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.settings.SettingsFilter; -import org.elasticsearch.operator.OperatorHandler; import org.elasticsearch.rest.RestController; import org.elasticsearch.rest.RestHandler; import org.elasticsearch.rest.RestHeaderDefinition; @@ -83,10 +82,6 @@ default List getRestHandlers( return Collections.emptyList(); } - default List> getOperatorHandlers() { - return Collections.emptyList(); - } - /** * Returns headers which should be copied through rest requests on to internal requests. */ diff --git a/server/src/main/java/org/elasticsearch/plugins/PluginsService.java b/server/src/main/java/org/elasticsearch/plugins/PluginsService.java index d5af93af4a2c4..d91d047cfaf5c 100644 --- a/server/src/main/java/org/elasticsearch/plugins/PluginsService.java +++ b/server/src/main/java/org/elasticsearch/plugins/PluginsService.java @@ -26,6 +26,7 @@ import org.elasticsearch.env.Environment; import org.elasticsearch.jdk.JarHell; import org.elasticsearch.node.ReportingService; +import org.elasticsearch.operator.OperatorHandlerProvider; import org.elasticsearch.plugins.spi.SPIClassIterator; import java.io.IOException; @@ -52,6 +53,7 @@ import java.util.Locale; import java.util.Map; import java.util.Objects; +import java.util.ServiceLoader; import java.util.Set; import java.util.function.Consumer; import java.util.function.Function; @@ -98,6 +100,8 @@ record LoadedPlugin(PluginDescriptor descriptor, Plugin instance, ClassLoader lo private final List plugins; private final PluginsAndModules info; + private static Class[] SPI_CLASSES = { OperatorHandlerProvider.class }; + public static final Setting> MANDATORY_SETTING = Setting.listSetting( "plugin.mandatory", Collections.emptyList(), @@ -281,7 +285,6 @@ private Map loadBundles(Set bundles) { // package-private for test visibility static void loadExtensions(Collection plugins) { - Map> extendingPluginsByName = plugins.stream() .flatMap(t -> t.descriptor().getExtendedPlugins().stream().map(extendedPlugin -> Tuple.tuple(extendedPlugin, t.instance()))) .collect(Collectors.groupingBy(Tuple::v1, Collectors.mapping(Tuple::v2, Collectors.toList()))); @@ -295,6 +298,17 @@ static void loadExtensions(Collection plugins) { } } + public List loadServiceProviders(Class provider) { + List result = new ArrayList<>(); + getClass().getModule().addUses(provider); + + for (LoadedPlugin pluginTuple : plugins) { + ServiceLoader.load(provider, pluginTuple.loader()).iterator().forEachRemaining(c -> result.add(c)); + } + + return Collections.unmodifiableList(result); + } + private static void loadExtensionsForPlugin(ExtensiblePlugin extensiblePlugin, List extendingPlugins) { ExtensiblePlugin.ExtensionLoader extensionLoader = new ExtensiblePlugin.ExtensionLoader() { @Override diff --git a/x-pack/plugin/ilm/src/main/java/module-info.java b/x-pack/plugin/ilm/src/main/java/module-info.java new file mode 100644 index 0000000000000..af6a442e46f4f --- /dev/null +++ b/x-pack/plugin/ilm/src/main/java/module-info.java @@ -0,0 +1,22 @@ +/* + * 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. + */ +module org.elasticsearch.ilm { + requires org.apache.lucene.core; + requires org.elasticsearch.server; + requires org.elasticsearch.base; + requires org.elasticsearch.xcore; + requires org.elasticsearch.xcontent; + requires org.apache.logging.log4j; + + exports org.elasticsearch.xpack.ilm.action to org.elasticsearch.server; + exports org.elasticsearch.xpack.ilm.operator.action to org.elasticsearch.server; + exports org.elasticsearch.xpack.ilm to org.elasticsearch.server; + exports org.elasticsearch.xpack.slm.action to org.elasticsearch.server; + exports org.elasticsearch.xpack.slm to org.elasticsearch.server; + + provides org.elasticsearch.operator.OperatorHandlerProvider with org.elasticsearch.xpack.ilm.operator.ILMOperatorHandlerProvider; +} diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycle.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycle.java index e6ce6179306e7..fc908b71ee8d4 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycle.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycle.java @@ -30,7 +30,6 @@ import org.elasticsearch.index.IndexModule; import org.elasticsearch.index.IndexSettings; import org.elasticsearch.license.XPackLicenseState; -import org.elasticsearch.operator.OperatorHandler; import org.elasticsearch.plugins.ActionPlugin; import org.elasticsearch.plugins.HealthPlugin; import org.elasticsearch.plugins.Plugin; @@ -106,9 +105,10 @@ import org.elasticsearch.xpack.ilm.action.TransportRetryAction; import org.elasticsearch.xpack.ilm.action.TransportStartILMAction; import org.elasticsearch.xpack.ilm.action.TransportStopILMAction; -import org.elasticsearch.xpack.ilm.action.operator.OperatorLifecycleAction; import org.elasticsearch.xpack.ilm.history.ILMHistoryStore; import org.elasticsearch.xpack.ilm.history.ILMHistoryTemplateRegistry; +import org.elasticsearch.xpack.ilm.operator.ILMOperatorHandlerProvider; +import org.elasticsearch.xpack.ilm.operator.action.OperatorLifecycleAction; import org.elasticsearch.xpack.slm.SLMInfoTransportAction; import org.elasticsearch.xpack.slm.SLMUsageTransportAction; import org.elasticsearch.xpack.slm.SlmHealthIndicatorService; @@ -271,6 +271,8 @@ public Collection createComponents( ilmHealthIndicatorService.set(new IlmHealthIndicatorService(clusterService)); slmHealthIndicatorService.set(new SlmHealthIndicatorService(clusterService)); ilmOperatorAction.set(new OperatorLifecycleAction(xContentRegistry, client, XPackPlugin.getSharedLicenseState())); + + ILMOperatorHandlerProvider.handler(ilmOperatorAction.get()); return components; } @@ -379,11 +381,6 @@ public List getRestHandlers( return handlers; } - @Override - public List> getOperatorHandlers() { - return List.of(ilmOperatorAction.get()); - } - @Override public List> getActions() { var ilmUsageAction = new ActionHandler<>(XPackUsageFeatureAction.INDEX_LIFECYCLE, IndexLifecycleUsageTransportAction.class); diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportDeleteLifecycleAction.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportDeleteLifecycleAction.java index 09e4faebf0710..c66188e3fd6e7 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportDeleteLifecycleAction.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportDeleteLifecycleAction.java @@ -29,7 +29,7 @@ import org.elasticsearch.xpack.core.ilm.LifecyclePolicyMetadata; import org.elasticsearch.xpack.core.ilm.action.DeleteLifecycleAction; import org.elasticsearch.xpack.core.ilm.action.DeleteLifecycleAction.Request; -import org.elasticsearch.xpack.ilm.action.operator.OperatorLifecycleAction; +import org.elasticsearch.xpack.ilm.operator.action.OperatorLifecycleAction; import java.util.List; import java.util.Optional; diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportPutLifecycleAction.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportPutLifecycleAction.java index 071d7cffe228a..d1d75d7333d85 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportPutLifecycleAction.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/TransportPutLifecycleAction.java @@ -41,7 +41,7 @@ import org.elasticsearch.xpack.core.ilm.action.PutLifecycleAction; import org.elasticsearch.xpack.core.ilm.action.PutLifecycleAction.Request; import org.elasticsearch.xpack.core.slm.SnapshotLifecycleMetadata; -import org.elasticsearch.xpack.ilm.action.operator.OperatorLifecycleAction; +import org.elasticsearch.xpack.ilm.operator.action.OperatorLifecycleAction; import java.time.Instant; import java.util.HashMap; diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/operator/ILMOperatorHandlerProvider.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/operator/ILMOperatorHandlerProvider.java new file mode 100644 index 0000000000000..be49478ce06d0 --- /dev/null +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/operator/ILMOperatorHandlerProvider.java @@ -0,0 +1,32 @@ +/* + * 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.ilm.operator; + +import org.elasticsearch.action.support.master.MasterNodeRequest; +import org.elasticsearch.operator.OperatorHandler; +import org.elasticsearch.operator.OperatorHandlerProvider; + +import java.util.Collection; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +/** + * ILM Provider implementation for the OperatorHandlerProvider service interface + */ +public class ILMOperatorHandlerProvider implements OperatorHandlerProvider { + private static final Set>> handlers = ConcurrentHashMap.newKeySet(); + + @Override + public Collection>> handlers() { + return handlers; + } + + public static void handler(OperatorHandler> handler) { + handlers.add(handler); + } +} diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/operator/OperatorLifecycleAction.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/operator/action/OperatorLifecycleAction.java similarity index 98% rename from x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/operator/OperatorLifecycleAction.java rename to x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/operator/action/OperatorLifecycleAction.java index ceec783d775d6..d3c54200320dd 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/action/operator/OperatorLifecycleAction.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/operator/action/OperatorLifecycleAction.java @@ -5,7 +5,7 @@ * 2.0. */ -package org.elasticsearch.xpack.ilm.action.operator; +package org.elasticsearch.xpack.ilm.operator.action; import org.elasticsearch.client.internal.Client; import org.elasticsearch.cluster.ClusterState; diff --git a/x-pack/plugin/ilm/src/main/resources/META-INF/services/org.elasticsearch.operator.OperatorHandlerProvider b/x-pack/plugin/ilm/src/main/resources/META-INF/services/org.elasticsearch.operator.OperatorHandlerProvider new file mode 100644 index 0000000000000..55bfd83cbf428 --- /dev/null +++ b/x-pack/plugin/ilm/src/main/resources/META-INF/services/org.elasticsearch.operator.OperatorHandlerProvider @@ -0,0 +1 @@ +org.elasticsearch.xpack.ilm.operator.ILMOperatorHandlerProvider diff --git a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/action/TransportDeleteLifecycleActionTests.java b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/action/TransportDeleteLifecycleActionTests.java index 18c3bb6989dab..51df10fb7a190 100644 --- a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/action/TransportDeleteLifecycleActionTests.java +++ b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/action/TransportDeleteLifecycleActionTests.java @@ -14,7 +14,7 @@ import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.TransportService; import org.elasticsearch.xpack.core.ilm.action.DeleteLifecycleAction; -import org.elasticsearch.xpack.ilm.action.operator.OperatorLifecycleAction; +import org.elasticsearch.xpack.ilm.operator.action.OperatorLifecycleAction; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.mockito.Mockito.mock; diff --git a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/action/TransportPutLifecycleActionTests.java b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/action/TransportPutLifecycleActionTests.java index 76b24a19dc6fd..6731957cb43ab 100644 --- a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/action/TransportPutLifecycleActionTests.java +++ b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/action/TransportPutLifecycleActionTests.java @@ -23,7 +23,7 @@ import org.elasticsearch.xpack.core.ilm.LifecyclePolicyMetadata; import org.elasticsearch.xpack.core.ilm.action.PutLifecycleAction; import org.elasticsearch.xpack.ilm.LifecyclePolicyTestsUtils; -import org.elasticsearch.xpack.ilm.action.operator.OperatorLifecycleAction; +import org.elasticsearch.xpack.ilm.operator.action.OperatorLifecycleAction; import java.util.Map; diff --git a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/action/operator/OperatorILMControllerTests.java b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/action/operator/OperatorILMControllerTests.java index ad6624740fd44..df262fdedc9db 100644 --- a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/action/operator/OperatorILMControllerTests.java +++ b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/action/operator/OperatorILMControllerTests.java @@ -42,6 +42,7 @@ import org.elasticsearch.xpack.core.ilm.TimeseriesLifecycleType; import org.elasticsearch.xpack.core.ilm.UnfollowAction; import org.elasticsearch.xpack.core.ilm.WaitForSnapshotAction; +import org.elasticsearch.xpack.ilm.operator.action.OperatorLifecycleAction; import java.io.IOException; import java.util.ArrayList; From eee70fbca0fa0f239ff3ad3a9a713c067a488e8c Mon Sep 17 00:00:00 2001 From: Nikola Grcevski Date: Tue, 14 Jun 2022 09:57:26 -0400 Subject: [PATCH 44/48] fix javadoc --- .../main/java/org/elasticsearch/operator/OperatorHandler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main/java/org/elasticsearch/operator/OperatorHandler.java b/server/src/main/java/org/elasticsearch/operator/OperatorHandler.java index 553fc60187a68..130396492c259 100644 --- a/server/src/main/java/org/elasticsearch/operator/OperatorHandler.java +++ b/server/src/main/java/org/elasticsearch/operator/OperatorHandler.java @@ -103,7 +103,7 @@ default void validate(T request) { * Convenience method that creates a {@link XContentParser} from a content map so that it can be passed to * existing REST based code for input parsing. * - * @config XContentParserConfiguration for this mapper + * @param config XContentParserConfiguration for this mapper * @param source the operator content as a map * @return */ From 602b33cb9ada6ba3c185f11cf9029a45b76951d2 Mon Sep 17 00:00:00 2001 From: Nikola Grcevski Date: Wed, 15 Jun 2022 10:50:37 -0400 Subject: [PATCH 45/48] Change to illegalargument exception --- .../master/TransportMasterNodeAction.java | 14 ++++++++------ .../master/TransportMasterNodeActionTests.java | 16 +++++++++++++++- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/action/support/master/TransportMasterNodeAction.java b/server/src/main/java/org/elasticsearch/action/support/master/TransportMasterNodeAction.java index 080b3ff4fe302..e6b97faa4abbd 100644 --- a/server/src/main/java/org/elasticsearch/action/support/master/TransportMasterNodeAction.java +++ b/server/src/main/java/org/elasticsearch/action/support/master/TransportMasterNodeAction.java @@ -152,14 +152,11 @@ protected Optional operatorHandlerName() { return Optional.empty(); } - boolean supportsOperatorSetState() { - return operatorHandlerName().isPresent(); - } - protected Set modifiedKeys(Request request) { return Collections.emptySet(); } + // package private for testing void validateForOperatorState(Request request, ClusterState state) { Optional handlerName = operatorHandlerName(); assert handlerName.isPresent(); @@ -175,16 +172,21 @@ void validateForOperatorState(Request request, ClusterState state) { } if (errors.isEmpty() == false) { - throw new IllegalStateException( + throw new IllegalArgumentException( format("Failed to process request [%s] with errors: %s", request, String.join(System.lineSeparator(), errors)) ); } } + // package private for testing + boolean supportsOperatorMode() { + return operatorHandlerName().isPresent(); + } + @Override protected void doExecute(Task task, final Request request, ActionListener listener) { ClusterState state = clusterService.state(); - if (supportsOperatorSetState()) { + if (supportsOperatorMode()) { validateForOperatorState(request, state); } logger.trace("starting processing request [{}] with cluster state version [{}]", request, state.version()); diff --git a/server/src/test/java/org/elasticsearch/action/support/master/TransportMasterNodeActionTests.java b/server/src/test/java/org/elasticsearch/action/support/master/TransportMasterNodeActionTests.java index e27c9aae3180e..b19b95b074a8e 100644 --- a/server/src/test/java/org/elasticsearch/action/support/master/TransportMasterNodeActionTests.java +++ b/server/src/test/java/org/elasticsearch/action/support/master/TransportMasterNodeActionTests.java @@ -258,6 +258,12 @@ protected void masterOperation(Task task, Request request, ClusterState state, A protected ClusterBlockException checkBlock(Request request, ClusterState state) { return null; // default implementation, overridden in specific tests } + } + + class OperatorAction extends Action { + OperatorAction(String actionName, TransportService transportService, ClusterService clusterService, ThreadPool threadPool) { + super(actionName, transportService, clusterService, threadPool, ThreadPool.Names.SAME); + } @Override protected Optional operatorHandlerName() { @@ -769,6 +775,12 @@ public void testRejectOperatorConflictClusterStateUpdate() { Action noHandler = new Action("internal:testAction", transportService, clusterService, threadPool, ThreadPool.Names.SAME); + assertFalse(noHandler.supportsOperatorMode()); + + noHandler = new OperatorAction("internal:testOpAction", transportService, clusterService, threadPool); + + assertTrue(noHandler.supportsOperatorMode()); + // nothing should happen here, since the request doesn't touch any of the operator key noHandler.validateForOperatorState(new Request(), clusterState); @@ -784,8 +796,10 @@ public void testRejectOperatorConflictClusterStateUpdate() { ThreadPool.Names.SAME ); + assertTrue(action.supportsOperatorMode()); + assertTrue( - expectThrows(IllegalStateException.class, () -> action.validateForOperatorState(request, clusterState)).getMessage() + expectThrows(IllegalArgumentException.class, () -> action.validateForOperatorState(request, clusterState)).getMessage() .contains("with errors: [a] set in operator mode by [namespace_one]\n" + "[e] set in operator mode by [namespace_two]") ); From f61b5b76f45f1cba968e901d0bd654f3e1cca19c Mon Sep 17 00:00:00 2001 From: Nikola Grcevski Date: Wed, 15 Jun 2022 15:38:55 -0400 Subject: [PATCH 46/48] Fail start on bad operator config --- .../master/TransportMasterNodeAction.java | 15 ++++++ .../operator/service/FileSettingsService.java | 30 +++++++++--- .../service/FileSettingsServiceTests.java | 46 ++++++++++++++++++- 3 files changed, 83 insertions(+), 8 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/action/support/master/TransportMasterNodeAction.java b/server/src/main/java/org/elasticsearch/action/support/master/TransportMasterNodeAction.java index e6b97faa4abbd..cad5c5cd3d62f 100644 --- a/server/src/main/java/org/elasticsearch/action/support/master/TransportMasterNodeAction.java +++ b/server/src/main/java/org/elasticsearch/action/support/master/TransportMasterNodeAction.java @@ -148,10 +148,25 @@ private ClusterBlockException checkBlockIfStateRecovered(Request request, Cluste } } + /** + * Override this method if the master node action also has an {@link org.elasticsearch.operator.OperatorHandler} + * interaction. We need to check if certain settings or entities are allowed to be modified by the master node + * action, depending on if they are set already in operator mode. + * + * @return an Optional of the operator handler name + */ protected Optional operatorHandlerName() { return Optional.empty(); } + /** + * Override this method to return the keys of the cluster state or cluster entities that are modified by + * the Request object. This method is used by the operator handler logic (see {@link org.elasticsearch.operator.OperatorHandler}) + * to verify if the keys don't conflict with an existing key set in operator mode. + * + * @param request the TransportMasterNode request + * @return set of String keys intended to be modified/set/deleted by this request + */ protected Set modifiedKeys(Request request) { return Collections.emptySet(); } diff --git a/server/src/main/java/org/elasticsearch/operator/service/FileSettingsService.java b/server/src/main/java/org/elasticsearch/operator/service/FileSettingsService.java index 263bb1cf3aca6..40b8ba69606d4 100644 --- a/server/src/main/java/org/elasticsearch/operator/service/FileSettingsService.java +++ b/server/src/main/java/org/elasticsearch/operator/service/FileSettingsService.java @@ -37,7 +37,6 @@ public class FileSettingsService extends AbstractLifecycleComponent implements C private static final String SETTINGS_FILE_NAME = "settings.json"; private static final String NAMESPACE = "file_settings"; - private final ClusterService clusterService; private final OperatorClusterStateController controller; private final Environment environment; @@ -55,7 +54,6 @@ public class FileSettingsService extends AbstractLifecycleComponent implements C ); public FileSettingsService(ClusterService clusterService, OperatorClusterStateController controller, Environment environment) { - this.clusterService = clusterService; this.controller = controller; this.environment = environment; clusterService.addListener(this); @@ -142,7 +140,7 @@ synchronized void startWatcher() { Path settingsFilePath = operatorSettingsFile(); if (Files.exists(settingsFilePath)) { logger.info("found initial operator settings file [{}], applying...", settingsFilePath); - processFileSettings(settingsFilePath); + processFileSettings(settingsFilePath, true); } enableSettingsWatcher(settingsDir); } else { @@ -194,7 +192,7 @@ synchronized void startWatcher() { enableSettingsWatcher(settingsDir); if (watchedFileChanged(path)) { - processFileSettings(path); + processFileSettings(path, false); } } catch (Exception e) { logger.warn("unable to watch or read operator settings file", e); @@ -220,7 +218,9 @@ synchronized void stopWatcher() { if (watching()) { try { watchService.close(); - watcherThreadLatch.await(); + if (watcherThreadLatch != null) { + watcherThreadLatch.await(); + } } catch (IOException | InterruptedException e) { logger.info("encountered exception while closing watch service", e); } finally { @@ -241,7 +241,7 @@ private void enableSettingsWatcher(Path settingsDir) throws IOException { ); } - void processFileSettings(Path path) { + void processFileSettings(Path path, boolean onStartup) { logger.info("processing path [{}] for [{}]", path, NAMESPACE); try ( XContentParser parser = XContentType.JSON.xContent().createParser(XContentParserConfiguration.EMPTY, Files.newInputStream(path)) @@ -251,7 +251,13 @@ void processFileSettings(Path path) { if (e instanceof OperatorClusterStateController.IncompatibleVersionException) { logger.info(e.getMessage()); } else { - logger.error("Error processing operator settings json file", e); + // If we encountered an exception trying to apply the operator state at + // startup time, we throw an error to force Elasticsearch to exit. + if (onStartup) { + throw new OperatorConfigurationError("Error applying operator settings", e); + } else { + logger.error("Error processing operator settings json file", e); + } } } }); @@ -261,4 +267,14 @@ void processFileSettings(Path path) { } record FileUpdateState(long timestamp, String path, Object fileKey) {} + + /** + * Error subclass that is thrown when we encounter a fatal error while applying + * the operator cluster state at Elasticsearch boot time. + */ + public static class OperatorConfigurationError extends Error { + public OperatorConfigurationError(String message, Throwable t) { + super(message, t); + } + } } diff --git a/server/src/test/java/org/elasticsearch/operator/service/FileSettingsServiceTests.java b/server/src/test/java/org/elasticsearch/operator/service/FileSettingsServiceTests.java index 4f1c263acee2c..84025b29efc89 100644 --- a/server/src/test/java/org/elasticsearch/operator/service/FileSettingsServiceTests.java +++ b/server/src/test/java/org/elasticsearch/operator/service/FileSettingsServiceTests.java @@ -31,9 +31,14 @@ import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -122,7 +127,7 @@ public void testCallsProcessing() throws Exception { doAnswer((Answer) invocation -> { processFileLatch.countDown(); return null; - }).when(service).processFileSettings(any()); + }).when(service).processFileSettings(any(), anyBoolean()); service.start(); service.startWatcher(); @@ -143,4 +148,43 @@ public void testCallsProcessing() throws Exception { service.close(); } + @SuppressWarnings("unchecked") + public void testInitialFile() throws Exception { + OperatorClusterStateController controller = mock(OperatorClusterStateController.class); + + doAnswer((Answer) invocation -> { + ((Consumer) invocation.getArgument(2)).accept(new IllegalStateException("Some exception")); + return null; + }).when(controller).process(any(), any(), any()); + + FileSettingsService service = spy(new FileSettingsService(clusterService, controller, env)); + + Files.createDirectories(service.operatorSettingsDir()); + + // contents of the JSON don't matter, we just need a file to exist + Files.write(service.operatorSettingsFile(), "{}".getBytes(StandardCharsets.UTF_8)); + + service.start(); + assertEquals( + "Error applying operator settings", + expectThrows(FileSettingsService.OperatorConfigurationError.class, () -> service.startWatcher()).getMessage() + ); + + verify(service, times(1)).processFileSettings(any(), eq(true)); + + service.stop(); + + clearInvocations(service); + + // Let's check that if we didn't throw an error that everything works + doAnswer((Answer) invocation -> null).when(controller).process(any(), any(), any()); + + service.start(); + service.startWatcher(); + + verify(service, times(1)).processFileSettings(any(), eq(true)); + + service.stop(); + service.close(); + } } From e81d77952a9e456ebf73040a46a883b44a1d3d83 Mon Sep 17 00:00:00 2001 From: Nikola Grcevski Date: Wed, 15 Jun 2022 20:51:17 -0400 Subject: [PATCH 47/48] Add version check on error metadata --- .../service/OperatorUpdateStateTask.java | 16 ++++ .../OperatorClusterStateControllerTests.java | 78 ++++++++++++++++++- 2 files changed, 93 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/org/elasticsearch/operator/service/OperatorUpdateStateTask.java b/server/src/main/java/org/elasticsearch/operator/service/OperatorUpdateStateTask.java index 8d2a141012db8..e993290a67d41 100644 --- a/server/src/main/java/org/elasticsearch/operator/service/OperatorUpdateStateTask.java +++ b/server/src/main/java/org/elasticsearch/operator/service/OperatorUpdateStateTask.java @@ -93,6 +93,22 @@ protected ClusterState execute(ClusterState state) { } if (errors.isEmpty() == false) { + // Check if we had previous error metadata with version information, don't spam with cluster state updates, if the + // version hasn't been updated. + if (existingMetadata != null + && existingMetadata.errorMetadata() != null + && existingMetadata.errorMetadata().version() >= stateVersionMetadata.version()) { + logger.error("Error processing state change request for [{}] with the following errors [{}]", namespace, errors); + + throw new OperatorClusterStateController.IncompatibleVersionException( + format( + "Not updating error state because version [%s] is less or equal to the last operator error version [%s]", + stateVersionMetadata.version(), + existingMetadata.errorMetadata().version() + ) + ); + } + recordErrorState.accept( new OperatorClusterStateController.OperatorErrorState( namespace, diff --git a/server/src/test/java/org/elasticsearch/operator/service/OperatorClusterStateControllerTests.java b/server/src/test/java/org/elasticsearch/operator/service/OperatorClusterStateControllerTests.java index 744f5fafe49e0..30042257bbf40 100644 --- a/server/src/test/java/org/elasticsearch/operator/service/OperatorClusterStateControllerTests.java +++ b/server/src/test/java/org/elasticsearch/operator/service/OperatorClusterStateControllerTests.java @@ -16,7 +16,9 @@ import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.ClusterStateAckListener; import org.elasticsearch.cluster.ClusterStateTaskExecutor; +import org.elasticsearch.cluster.metadata.Metadata; import org.elasticsearch.cluster.metadata.OperatorErrorMetadata; +import org.elasticsearch.cluster.metadata.OperatorHandlerMetadata; import org.elasticsearch.cluster.metadata.OperatorMetadata; import org.elasticsearch.cluster.routing.RerouteService; import org.elasticsearch.cluster.service.ClusterService; @@ -34,6 +36,7 @@ import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; @@ -246,7 +249,80 @@ public void onFailure(Exception failure) {} assertThat(operatorMetadata.errorMetadata().errors(), contains("some parse error", "some io error")); } - public void testcheckMetadataVersion() { + public void testUpdateTaskDuplicateError() { + OperatorHandler dummy = new OperatorHandler<>() { + @Override + public String name() { + return "one"; + } + + @Override + public TransformState transform(Object source, TransformState prevState) throws Exception { + throw new Exception("anything"); + } + }; + + OperatorUpdateStateTask task = spy( + new OperatorUpdateStateTask( + "namespace_one", + new OperatorClusterStateController.SettingsFile( + Map.of("one", "two"), + new OperatorStateVersionMetadata(1L, Version.CURRENT) + ), + Map.of("one", dummy), + List.of(dummy.name()), + (errorState) -> {}, + new ActionListener<>() { + @Override + public void onResponse(ActionResponse.Empty empty) {} + + @Override + public void onFailure(Exception e) {} + } + ) + ); + + OperatorHandlerMetadata hmOne = new OperatorHandlerMetadata.Builder("one").keys(Set.of("a", "b")).build(); + + OperatorErrorMetadata emOne = new OperatorErrorMetadata.Builder().version(1L) + .errorKind(OperatorErrorMetadata.ErrorKind.VALIDATION) + .errors(List.of("Test error 1", "Test error 2")) + .build(); + + OperatorMetadata operatorMetadata = OperatorMetadata.builder("namespace_one") + .errorMetadata(emOne) + .version(1L) + .putHandler(hmOne) + .build(); + + Metadata metadata = Metadata.builder().putOperatorState(operatorMetadata).build(); + ClusterState state = ClusterState.builder(new ClusterName("test")).metadata(metadata).build(); + + // We exit on duplicate errors before we update the cluster state error metadata + assertEquals( + "Not updating error state because version [1] is less or equal to the last operator error version [1]", + expectThrows(OperatorClusterStateController.IncompatibleVersionException.class, () -> task.execute(state)).getMessage() + ); + + emOne = new OperatorErrorMetadata.Builder().version(0L) + .errorKind(OperatorErrorMetadata.ErrorKind.VALIDATION) + .errors(List.of("Test error 1", "Test error 2")) + .build(); + + // If we are writing with older error metadata, we should get proper IllegalStateException + operatorMetadata = OperatorMetadata.builder("namespace_one").errorMetadata(emOne).version(0L).putHandler(hmOne).build(); + + metadata = Metadata.builder().putOperatorState(operatorMetadata).build(); + ClusterState newState = ClusterState.builder(new ClusterName("test")).metadata(metadata).build(); + + // We exit on duplicate errors before we update the cluster state error metadata + assertEquals( + "Error processing state change request for namespace_one", + expectThrows(IllegalStateException.class, () -> task.execute(newState)).getMessage() + ); + } + + public void testCheckMetadataVersion() { OperatorMetadata operatorMetadata = OperatorMetadata.builder("test").version(123L).build(); assertTrue( From 4272392e0224f7fbc579ae78a5dae0fb9f50ec29 Mon Sep 17 00:00:00 2001 From: Nikola Grcevski Date: Thu, 16 Jun 2022 10:10:46 -0400 Subject: [PATCH 48/48] Refactor ilm section to exclude policy --- .../operator/OperatorHandler.java | 4 +- .../operator/OperatorHandlerProvider.java | 4 +- .../operator/ILMOperatorHandlerProvider.java | 7 +- .../action/OperatorLifecycleAction.java | 10 ++- .../operator/OperatorILMControllerTests.java | 82 ++++++++----------- 5 files changed, 48 insertions(+), 59 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/operator/OperatorHandler.java b/server/src/main/java/org/elasticsearch/operator/OperatorHandler.java index 130396492c259..08151bc2ba474 100644 --- a/server/src/main/java/org/elasticsearch/operator/OperatorHandler.java +++ b/server/src/main/java/org/elasticsearch/operator/OperatorHandler.java @@ -28,7 +28,7 @@ * that we have a separate update handler interface to the REST handlers. This interface declares * the basic contract for implementing cluster state update handlers in operator mode. */ -public interface OperatorHandler> { +public interface OperatorHandler { String CONTENT = "content"; /** @@ -78,7 +78,7 @@ default Collection dependencies() { * * @param request the master node request that we base this operator handler on */ - default void validate(T request) { + default void validate(MasterNodeRequest request) { ActionRequestValidationException exception = request.validate(); if (exception != null) { throw new IllegalStateException("Validation error", exception); diff --git a/server/src/main/java/org/elasticsearch/operator/OperatorHandlerProvider.java b/server/src/main/java/org/elasticsearch/operator/OperatorHandlerProvider.java index 3446a60797e68..0f168ea6e4d61 100644 --- a/server/src/main/java/org/elasticsearch/operator/OperatorHandlerProvider.java +++ b/server/src/main/java/org/elasticsearch/operator/OperatorHandlerProvider.java @@ -8,8 +8,6 @@ package org.elasticsearch.operator; -import org.elasticsearch.action.support.master.MasterNodeRequest; - import java.util.Collection; /** @@ -23,5 +21,5 @@ public interface OperatorHandlerProvider { * * @return a list of ${@link OperatorHandler}s */ - Collection>> handlers(); + Collection> handlers(); } diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/operator/ILMOperatorHandlerProvider.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/operator/ILMOperatorHandlerProvider.java index be49478ce06d0..5577401719cfd 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/operator/ILMOperatorHandlerProvider.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/operator/ILMOperatorHandlerProvider.java @@ -7,7 +7,6 @@ package org.elasticsearch.xpack.ilm.operator; -import org.elasticsearch.action.support.master.MasterNodeRequest; import org.elasticsearch.operator.OperatorHandler; import org.elasticsearch.operator.OperatorHandlerProvider; @@ -19,14 +18,14 @@ * ILM Provider implementation for the OperatorHandlerProvider service interface */ public class ILMOperatorHandlerProvider implements OperatorHandlerProvider { - private static final Set>> handlers = ConcurrentHashMap.newKeySet(); + private static final Set> handlers = ConcurrentHashMap.newKeySet(); @Override - public Collection>> handlers() { + public Collection> handlers() { return handlers; } - public static void handler(OperatorHandler> handler) { + public static void handler(OperatorHandler handler) { handlers.add(handler); } } diff --git a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/operator/action/OperatorLifecycleAction.java b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/operator/action/OperatorLifecycleAction.java index d3c54200320dd..d5df7d37595d6 100644 --- a/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/operator/action/OperatorLifecycleAction.java +++ b/x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/operator/action/OperatorLifecycleAction.java @@ -15,6 +15,7 @@ import org.elasticsearch.xcontent.NamedXContentRegistry; import org.elasticsearch.xcontent.XContentParser; import org.elasticsearch.xcontent.XContentParserConfiguration; +import org.elasticsearch.xpack.core.ilm.LifecyclePolicy; import org.elasticsearch.xpack.core.ilm.action.PutLifecycleAction; import org.elasticsearch.xpack.core.template.LifecyclePolicyConfig; import org.elasticsearch.xpack.ilm.action.TransportDeleteLifecycleAction; @@ -30,9 +31,11 @@ import java.util.stream.Collectors; /** - * TODO: Add docs + * This {@link OperatorHandler} is responsible for CRUD operations on ILM policies in + * operator mode, e.g. file based settings. Internally it uses {@link TransportPutLifecycleAction} and + * {@link TransportDeleteLifecycleAction} to add, update and delete ILM policies. */ -public class OperatorLifecycleAction implements OperatorHandler { +public class OperatorLifecycleAction implements OperatorHandler { private final NamedXContentRegistry xContentRegistry; private final Client client; @@ -61,7 +64,8 @@ public Collection prepare(Object input) throws IOExc Map content = (Map) source.get(name); var config = XContentParserConfiguration.EMPTY.withRegistry(LifecyclePolicyConfig.DEFAULT_X_CONTENT_REGISTRY); try (XContentParser parser = mapToXContentParser(config, content)) { - PutLifecycleAction.Request request = PutLifecycleAction.Request.parseRequest(name, parser); + LifecyclePolicy policy = LifecyclePolicy.parse(parser, name); + PutLifecycleAction.Request request = new PutLifecycleAction.Request(policy); validate(request); result.add(request); } diff --git a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/action/operator/OperatorILMControllerTests.java b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/action/operator/OperatorILMControllerTests.java index df262fdedc9db..5472e61e719c1 100644 --- a/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/action/operator/OperatorILMControllerTests.java +++ b/x-pack/plugin/ilm/src/test/java/org/elasticsearch/xpack/ilm/action/operator/OperatorILMControllerTests.java @@ -110,12 +110,10 @@ public void testValidationFails() { String badPolicyJSON = """ { "my_timeseries_lifecycle": { - "polcy": { - "phases": { - "warm": { - "min_age": "10s", - "actions": { - } + "phase": { + "warm": { + "min_age": "10s", + "actions": { } } } @@ -123,7 +121,7 @@ public void testValidationFails() { }"""; assertEquals( - "[1:2] [put_lifecycle_request] unknown field [polcy] did you mean [policy]?", + "[1:2] [lifecycle_policy] unknown field [phase] did you mean [phases]?", expectThrows(XContentParseException.class, () -> processJSON(action, prevState, badPolicyJSON)).getMessage() ); } @@ -148,28 +146,24 @@ public void testActionAddRemove() throws Exception { String twoPoliciesJSON = """ { "my_timeseries_lifecycle": { - "policy": { - "phases": { - "warm": { - "min_age": "10s", - "actions": { - } + "phases": { + "warm": { + "min_age": "10s", + "actions": { } } } }, "my_timeseries_lifecycle1": { - "policy": { - "phases": { - "warm": { - "min_age": "10s", - "actions": { - } - }, - "delete": { - "min_age": "30s", - "actions": { - } + "phases": { + "warm": { + "min_age": "10s", + "actions": { + } + }, + "delete": { + "min_age": "30s", + "actions": { } } } @@ -187,12 +181,10 @@ public void testActionAddRemove() throws Exception { String onePolicyRemovedJSON = """ { "my_timeseries_lifecycle": { - "policy": { - "phases": { - "warm": { - "min_age": "10s", - "actions": { - } + "phases": { + "warm": { + "min_age": "10s", + "actions": { } } } @@ -208,12 +200,10 @@ public void testActionAddRemove() throws Exception { String onePolicyRenamedJSON = """ { "my_timeseries_lifecycle2": { - "policy": { - "phases": { - "warm": { - "min_age": "10s", - "actions": { - } + "phases": { + "warm": { + "min_age": "10s", + "actions": { } } } @@ -250,17 +240,15 @@ public void testOperatorController() throws IOException { }, "ilm": { "my_timeseries_lifecycle": { - "policy": { - "phases": { - "warm": { - "min_age": "10s", - "actions": { - } - }, - "delete": { - "min_age": "30s", - "actions": { - } + "phases": { + "warm": { + "min_age": "10s", + "actions": { + } + }, + "delete": { + "min_age": "30s", + "actions": { } } }