You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@solr.apache.org by cp...@apache.org on 2022/08/15 16:24:47 UTC
[solr] branch main updated: SOLR-16282: Update core admin handler to use pluggable custom actions (#931)
This is an automated email from the ASF dual-hosted git repository.
cpoerschke pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/solr.git
The following commit(s) were added to refs/heads/main by this push:
new 1a70dcac589 SOLR-16282: Update core admin handler to use pluggable custom actions (#931)
1a70dcac589 is described below
commit 1a70dcac5894db9652c6178a8bf7056d86d22f53
Author: ijioio <62...@users.noreply.github.com>
AuthorDate: Tue Aug 16 01:24:42 2022 +0900
SOLR-16282: Update core admin handler to use pluggable custom actions (#931)
Co-authored-by: Christine Poerschke <cp...@apache.org>
---
solr/CHANGES.txt | 1 +
.../java/org/apache/solr/core/CoreContainer.java | 12 ++
.../src/java/org/apache/solr/core/NodeConfig.java | 17 +++
.../java/org/apache/solr/core/SolrXmlConfig.java | 13 ++
.../solr/handler/admin/CoreAdminHandler.java | 56 +++++++--
solr/core/src/test-files/solr/solr-50-all.xml | 6 +
.../src/test/org/apache/solr/core/TestSolrXml.java | 43 +++++++
.../handler/admin/CoreAdminHandlerActionTest.java | 132 +++++++++++++++++++++
.../pages/configuring-solr-xml.adoc | 32 ++++-
9 files changed, 297 insertions(+), 15 deletions(-)
diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index 1a78a4e812d..4dde68eff48 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -45,6 +45,7 @@ New Features
* SOLR-14319: Add ability to specify replica types when creating collections in Admin UI. (Richard Goodman, Eric Pugh)
+* SOLR-16282: CoreAdminHandler supports custom actions via solr.xml configuration. (Artem Abeleshev, Christine Poerschke)
Improvements
---------------------
diff --git a/solr/core/src/java/org/apache/solr/core/CoreContainer.java b/solr/core/src/java/org/apache/solr/core/CoreContainer.java
index ab0ad0d81e3..e608865e200 100644
--- a/solr/core/src/java/org/apache/solr/core/CoreContainer.java
+++ b/solr/core/src/java/org/apache/solr/core/CoreContainer.java
@@ -57,6 +57,7 @@ import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.TimeoutException;
import java.util.function.Supplier;
+import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.auth.AuthSchemeProvider;
import org.apache.http.client.CredentialsProvider;
@@ -111,6 +112,7 @@ import org.apache.solr.handler.admin.CollectionsHandler;
import org.apache.solr.handler.admin.ConfigSetsHandler;
import org.apache.solr.handler.admin.ContainerPluginsApi;
import org.apache.solr.handler.admin.CoreAdminHandler;
+import org.apache.solr.handler.admin.CoreAdminHandler.CoreAdminOp;
import org.apache.solr.handler.admin.HealthCheckHandler;
import org.apache.solr.handler.admin.InfoHandler;
import org.apache.solr.handler.admin.MetricsHandler;
@@ -830,6 +832,16 @@ public class CoreContainer {
coreAdminHandler =
createHandler(CORES_HANDLER_PATH, cfg.getCoreAdminHandlerClass(), CoreAdminHandler.class);
+ Map<String, CoreAdminOp> coreAdminHandlerActions =
+ cfg.getCoreAdminHandlerActions().entrySet().stream()
+ .collect(
+ Collectors.toMap(
+ item -> item.getKey(),
+ item -> loader.newInstance(item.getValue(), CoreAdminOp.class)));
+
+ // Register custom actions for CoreAdminHandler
+ coreAdminHandler.registerCustomActions(coreAdminHandlerActions);
+
metricsHandler = new MetricsHandler(this);
containerHandlers.put(METRICS_PATH, metricsHandler);
metricsHandler.initializeMetrics(solrMetricsContext, METRICS_PATH);
diff --git a/solr/core/src/java/org/apache/solr/core/NodeConfig.java b/solr/core/src/java/org/apache/solr/core/NodeConfig.java
index 114ac3b8fef..10bd39ec55c 100644
--- a/solr/core/src/java/org/apache/solr/core/NodeConfig.java
+++ b/solr/core/src/java/org/apache/solr/core/NodeConfig.java
@@ -28,6 +28,7 @@ import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
+import java.util.Map;
import java.util.Properties;
import java.util.Set;
import org.apache.commons.lang3.StringUtils;
@@ -73,6 +74,8 @@ public class NodeConfig {
private final String coreAdminHandlerClass;
+ private final Map<String, String> coreAdminHandlerActions;
+
private final String collectionsAdminHandlerClass;
private final String healthCheckHandlerClass;
@@ -121,6 +124,7 @@ public class NodeConfig {
PluginInfo shardHandlerFactoryConfig,
UpdateShardHandlerConfig updateShardHandlerConfig,
String coreAdminHandlerClass,
+ Map<String, String> coreAdminHandlerActions,
String collectionsAdminHandlerClass,
String healthCheckHandlerClass,
String infoHandlerClass,
@@ -155,6 +159,7 @@ public class NodeConfig {
this.shardHandlerFactoryConfig = shardHandlerFactoryConfig;
this.updateShardHandlerConfig = updateShardHandlerConfig;
this.coreAdminHandlerClass = coreAdminHandlerClass;
+ this.coreAdminHandlerActions = coreAdminHandlerActions;
this.collectionsAdminHandlerClass = collectionsAdminHandlerClass;
this.healthCheckHandlerClass = healthCheckHandlerClass;
this.infoHandlerClass = infoHandlerClass;
@@ -304,6 +309,10 @@ public class NodeConfig {
return coreAdminHandlerClass;
}
+ public Map<String, String> getCoreAdminHandlerActions() {
+ return coreAdminHandlerActions;
+ }
+
public String getCollectionsHandlerClass() {
return collectionsAdminHandlerClass;
}
@@ -517,6 +526,7 @@ public class NodeConfig {
private UpdateShardHandlerConfig updateShardHandlerConfig = UpdateShardHandlerConfig.DEFAULT;
private String configSetServiceClass;
private String coreAdminHandlerClass = DEFAULT_ADMINHANDLERCLASS;
+ private Map<String, String> coreAdminHandlerActions = Collections.emptyMap();
private String collectionsAdminHandlerClass = DEFAULT_COLLECTIONSHANDLERCLASS;
private String healthCheckHandlerClass = DEFAULT_HEALTHCHECKHANDLERCLASS;
private String infoHandlerClass = DEFAULT_INFOHANDLERCLASS;
@@ -628,6 +638,12 @@ public class NodeConfig {
return this;
}
+ public NodeConfigBuilder setCoreAdminHandlerActions(
+ Map<String, String> coreAdminHandlerActions) {
+ this.coreAdminHandlerActions = coreAdminHandlerActions;
+ return this;
+ }
+
public NodeConfigBuilder setCollectionsAdminHandlerClass(String collectionsAdminHandlerClass) {
this.collectionsAdminHandlerClass = collectionsAdminHandlerClass;
return this;
@@ -762,6 +778,7 @@ public class NodeConfig {
shardHandlerFactoryConfig,
updateShardHandlerConfig,
coreAdminHandlerClass,
+ coreAdminHandlerActions,
collectionsAdminHandlerClass,
healthCheckHandlerClass,
infoHandlerClass,
diff --git a/solr/core/src/java/org/apache/solr/core/SolrXmlConfig.java b/solr/core/src/java/org/apache/solr/core/SolrXmlConfig.java
index ef327c3b69f..65d06d65c9d 100644
--- a/solr/core/src/java/org/apache/solr/core/SolrXmlConfig.java
+++ b/solr/core/src/java/org/apache/solr/core/SolrXmlConfig.java
@@ -35,6 +35,7 @@ import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.regex.Pattern;
+import java.util.stream.Collectors;
import javax.management.MBeanServer;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
@@ -132,6 +133,16 @@ public class SolrXmlConfig {
String nodeName = (String) entries.remove("nodeName");
if (Strings.isNullOrEmpty(nodeName) && cloudConfig != null) nodeName = cloudConfig.getHost();
+ // It should goes inside the fillSolrSection method but
+ // since it is arranged as a separate section it is placed here
+ Map<String, String> coreAdminHandlerActions =
+ readNodeListAsNamedList(
+ config, "solr/coreAdminHandlerActions/*[@name]", "<coreAdminHandlerActions>")
+ .asMap()
+ .entrySet()
+ .stream()
+ .collect(Collectors.toMap(item -> item.getKey(), item -> item.getValue().toString()));
+
UpdateShardHandlerConfig updateConfig;
if (deprecatedUpdateConfig == null) {
updateConfig =
@@ -168,6 +179,7 @@ public class SolrXmlConfig {
configBuilder.setMetricsConfig(getMetricsConfig(config));
configBuilder.setFromZookeeper(fromZookeeper);
configBuilder.setDefaultZkHost(defaultZkHost);
+ configBuilder.setCoreAdminHandlerActions(coreAdminHandlerActions);
return fillSolrSection(configBuilder, entries);
}
@@ -246,6 +258,7 @@ public class SolrXmlConfig {
assertSingleInstance("logging", config);
assertSingleInstance("logging/watcher", config);
assertSingleInstance("backup", config);
+ assertSingleInstance("coreAdminHandlerActions", config);
}
private static void assertSingleInstance(String section, XmlConfigFile config) {
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/CoreAdminHandler.java b/solr/core/src/java/org/apache/solr/handler/admin/CoreAdminHandler.java
index 1da2030e0b7..eac72512df4 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/CoreAdminHandler.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/CoreAdminHandler.java
@@ -33,6 +33,7 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
+import java.util.Map.Entry;
import java.util.concurrent.ExecutorService;
import org.apache.commons.lang3.StringUtils;
import org.apache.solr.api.AnnotatedApi;
@@ -154,6 +155,30 @@ public class CoreAdminHandler extends RequestHandlerBase implements PermissionNa
return Boolean.TRUE;
}
+ /**
+ * Registers custom actions defined in {@code solr.xml}. Called from the {@link CoreContainer}
+ * during load process.
+ *
+ * @param customActions to register
+ * @throws SolrException in case of action with indicated name is already registered
+ */
+ public final void registerCustomActions(Map<String, CoreAdminOp> customActions) {
+
+ for (Entry<String, CoreAdminOp> entry : customActions.entrySet()) {
+
+ String action = entry.getKey().toLowerCase(Locale.ROOT);
+ CoreAdminOp operation = entry.getValue();
+
+ if (opMap.containsKey(action)) {
+ throw new SolrException(
+ SolrException.ErrorCode.SERVER_ERROR,
+ "CoreAdminHandler already registered action " + action);
+ }
+
+ opMap.put(action, operation);
+ }
+ }
+
/**
* The instance of CoreContainer this handler handles. This should be the CoreContainer instance
* that created this handler.
@@ -190,8 +215,13 @@ public class CoreAdminHandler extends RequestHandlerBase implements PermissionNa
// Pick the action
final String action = req.getParams().get(ACTION, STATUS.toString()).toLowerCase(Locale.ROOT);
- CoreAdminOperation op = opMap.get(action);
+ CoreAdminOp op = opMap.get(action);
if (op == null) {
+ log.warn(
+ "action '{}' not found, calling custom action handler. "
+ + "If original intention was to target some custom behaviour "
+ + "use custom actions defined in 'solr.xml' instead",
+ action);
handleCustomAction(req, rsp);
return;
}
@@ -206,7 +236,7 @@ public class CoreAdminHandler extends RequestHandlerBase implements PermissionNa
} else {
try {
MDC.put("CoreAdminHandler.asyncId", taskId);
- MDC.put("CoreAdminHandler.action", op.action.toString());
+ MDC.put("CoreAdminHandler.action", action);
parallelExecutor.execute(
() -> {
boolean exceptionCaught = false;
@@ -242,7 +272,10 @@ public class CoreAdminHandler extends RequestHandlerBase implements PermissionNa
* <p>This method could be overridden by derived classes to handle custom actions. <br>
* By default - this method throws a solr exception. Derived classes are free to write their
* derivation if necessary.
+ *
+ * @deprecated Use actions defined via {@code solr.xml} instead.
*/
+ @Deprecated
protected void handleCustomAction(SolrQueryRequest req, SolrQueryResponse rsp) {
throw new SolrException(
SolrException.ErrorCode.BAD_REQUEST,
@@ -399,19 +432,16 @@ public class CoreAdminHandler extends RequestHandlerBase implements PermissionNa
if (parallelExecutor != null) ExecutorUtil.shutdownAndAwaitTermination(parallelExecutor);
}
- private static final Map<String, CoreAdminOperation> opMap = new HashMap<>();
+ private static final Map<String, CoreAdminOp> opMap = new HashMap<>();
- static class CallInfo {
- final CoreAdminHandler handler;
- final SolrQueryRequest req;
- final SolrQueryResponse rsp;
- final CoreAdminOperation op;
+ public static class CallInfo {
+ public final CoreAdminHandler handler;
+ public final SolrQueryRequest req;
+ public final SolrQueryResponse rsp;
+ public final CoreAdminOp op;
CallInfo(
- CoreAdminHandler handler,
- SolrQueryRequest req,
- SolrQueryResponse rsp,
- CoreAdminOperation op) {
+ CoreAdminHandler handler, SolrQueryRequest req, SolrQueryResponse rsp, CoreAdminOp op) {
this.handler = handler;
this.req = req;
this.rsp = rsp;
@@ -458,7 +488,7 @@ public class CoreAdminHandler extends RequestHandlerBase implements PermissionNa
Map<String, Object> invoke(SolrQueryRequest req);
}
- interface CoreAdminOp {
+ public interface CoreAdminOp {
/**
* @param it request/response object
* <p>If the request is invalid throw a SolrException with
diff --git a/solr/core/src/test-files/solr/solr-50-all.xml b/solr/core/src/test-files/solr/solr-50-all.xml
index 736349f2407..b7f6f260075 100644
--- a/solr/core/src/test-files/solr/solr-50-all.xml
+++ b/solr/core/src/test-files/solr/solr-50-all.xml
@@ -30,6 +30,12 @@
<int name="replayUpdatesThreads">100</int>
<int name="maxBooleanClauses">42</int>
+ <coreAdminHandlerActions>
+ <str name="action1">testCoreAdminHandlerAction1</str>
+ <str name="action2">testCoreAdminHandlerAction2</str>
+ <str name="action3">testCoreAdminHandlerAction3</str>
+ </coreAdminHandlerActions>
+
<solrcloud>
<int name="distribUpdateConnTimeout">22</int>
<int name="distribUpdateSoTimeout">33</int>
diff --git a/solr/core/src/test/org/apache/solr/core/TestSolrXml.java b/solr/core/src/test/org/apache/solr/core/TestSolrXml.java
index 0eda0db0567..24ae9aa3985 100644
--- a/solr/core/src/test/org/apache/solr/core/TestSolrXml.java
+++ b/solr/core/src/test/org/apache/solr/core/TestSolrXml.java
@@ -24,6 +24,7 @@ import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Locale;
+import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.stream.Collectors;
@@ -65,6 +66,16 @@ public class TestSolrXml extends SolrTestCaseJ4 {
assertEquals("maxBooleanClauses", (Integer) 42, cfg.getBooleanQueryMaxClauseCount());
assertEquals("core admin handler class", "testAdminHandler", cfg.getCoreAdminHandlerClass());
+ assertEquals(
+ "core admin handler actions",
+ Map.of(
+ "action1",
+ "testCoreAdminHandlerAction1",
+ "action2",
+ "testCoreAdminHandlerAction2",
+ "action3",
+ "testCoreAdminHandlerAction3"),
+ cfg.getCoreAdminHandlerActions());
assertEquals(
"collection handler class", "testCollectionsHandler", cfg.getCollectionsHandlerClass());
assertEquals("info handler class", "testInfoHandler", cfg.getInfoHandlerClass());
@@ -219,6 +230,18 @@ public class TestSolrXml extends SolrTestCaseJ4 {
SolrXmlConfig.fromString(solrHome, solrXml); // return not used, only for validation
}
+ public void testMultiCoreAdminHandlerActionsSectionError() {
+ String solrXml =
+ "<solr>"
+ + "<coreAdminHandlerActions><str name=\"action1\">testCoreAdminHandlerAction1</str></coreAdminHandlerActions>"
+ + "<coreAdminHandlerActions><str name=\"action2\">testCoreAdminHandlerAction2</str></coreAdminHandlerActions>"
+ + "</solr>";
+ expectedException.expect(SolrException.class);
+ expectedException.expectMessage(
+ "Multiple instances of coreAdminHandlerActions section found in solr.xml");
+ SolrXmlConfig.fromString(solrHome, solrXml); // return not used, only for validation
+ }
+
public void testValidStringValueWhenBoolTypeIsExpected() {
boolean schemaCache = random().nextBoolean();
String solrXml =
@@ -382,6 +405,26 @@ public class TestSolrXml extends SolrTestCaseJ4 {
SolrXmlConfig.fromString(solrHome, solrXml); // return not used, only for validation
}
+ public void testFailAtConfigParseTimeWhenCoreAdminHandlerActionsConfigParamsAreDuplicated() {
+ String v1 = "" + random().nextInt();
+ String v2 = "" + random().nextInt();
+ String solrXml =
+ String.format(
+ Locale.ROOT,
+ "<solr><coreAdminHandlerActions>"
+ + "<str name=\"action\">%s</str>"
+ + "<str name=\"action\">%s</str>"
+ + "</coreAdminHandlerActions></solr>",
+ v1,
+ v2);
+
+ expectedException.expect(SolrException.class);
+ expectedException.expectMessage(
+ "<coreAdminHandlerActions> section of solr.xml contains duplicated 'action'");
+
+ SolrXmlConfig.fromString(solrHome, solrXml); // return not used, only for validation
+ }
+
public void testCloudConfigRequiresHostPort() {
expectedException.expect(SolrException.class);
expectedException.expectMessage("solrcloud section missing required entry 'hostPort'");
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/CoreAdminHandlerActionTest.java b/solr/core/src/test/org/apache/solr/handler/admin/CoreAdminHandlerActionTest.java
new file mode 100644
index 00000000000..58ca65a795b
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/handler/admin/CoreAdminHandlerActionTest.java
@@ -0,0 +1,132 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.solr.handler.admin;
+
+import java.util.Locale;
+import java.util.Map;
+import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.params.CoreAdminParams;
+import org.apache.solr.common.util.NamedList;
+import org.apache.solr.common.util.SimpleOrderedMap;
+import org.apache.solr.handler.admin.CoreAdminHandler.CallInfo;
+import org.apache.solr.handler.admin.CoreAdminHandler.CoreAdminOp;
+import org.apache.solr.response.SolrQueryResponse;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+public class CoreAdminHandlerActionTest extends SolrTestCaseJ4 {
+
+ private static final String SOLR_XML =
+ "<solr><coreAdminHandlerActions>"
+ + "<str name='action1'>"
+ + CoreAdminHandlerTestAction1.class.getName()
+ + "</str>"
+ + "<str name='action2'>"
+ + CoreAdminHandlerTestAction2.class.getName()
+ + "</str>"
+ + "</coreAdminHandlerActions></solr>";
+
+ private static CoreAdminHandler admin = null;
+
+ @BeforeClass
+ public static void beforeClass() throws Exception {
+
+ setupNoCoreTest(createTempDir(), SOLR_XML);
+
+ admin = new CoreAdminHandler(h.getCoreContainer());
+ }
+
+ @Test
+ public void testRegisteredAction() throws Exception {
+
+ testAction(
+ "action1",
+ CoreAdminHandlerTestAction1.PROPERTY_NAME,
+ CoreAdminHandlerTestAction1.PROPERTY_VALUE);
+ testAction(
+ "action2",
+ CoreAdminHandlerTestAction2.PROPERTY_NAME,
+ CoreAdminHandlerTestAction2.PROPERTY_VALUE);
+ }
+
+ @Test
+ public void testUnregisteredAction() throws Exception {
+
+ SolrQueryResponse response = new SolrQueryResponse();
+
+ assertThrows(
+ "Attempt to execute unregistered action should throw SolrException",
+ SolrException.class,
+ () -> admin.handleRequestBody(req(CoreAdminParams.ACTION, "action3"), response));
+ }
+
+ @SuppressWarnings("unchecked")
+ private void testAction(String action, String propertyName, String propertyValue)
+ throws Exception {
+
+ SolrQueryResponse response = new SolrQueryResponse();
+
+ admin.handleRequestBody(req(CoreAdminParams.ACTION, action), response);
+
+ Map<String, Object> actionResponse = ((NamedList<Object>) response.getResponse()).asMap();
+
+ assertTrue(
+ String.format(Locale.ROOT, "Action response should contain %s property", propertyName),
+ actionResponse.containsKey(propertyName));
+ assertEquals(
+ String.format(
+ Locale.ROOT,
+ "Action response should contain %s value for %s property",
+ propertyValue,
+ propertyName),
+ propertyValue,
+ actionResponse.get(propertyName));
+ }
+
+ public static class CoreAdminHandlerTestAction1 implements CoreAdminOp {
+
+ public static final String PROPERTY_NAME = "property" + random().nextInt();
+ public static final String PROPERTY_VALUE = "value" + random().nextInt();
+
+ @Override
+ public void execute(CallInfo it) throws Exception {
+
+ NamedList<Object> details = new SimpleOrderedMap<>();
+
+ details.add(PROPERTY_NAME, PROPERTY_VALUE);
+
+ it.rsp.addResponse(details);
+ }
+ }
+
+ public static class CoreAdminHandlerTestAction2 implements CoreAdminOp {
+
+ public static final String PROPERTY_NAME = "property" + random().nextInt();
+ public static final String PROPERTY_VALUE = "value" + random().nextInt();
+
+ @Override
+ public void execute(CallInfo it) throws Exception {
+
+ NamedList<Object> details = new SimpleOrderedMap<>();
+
+ details.add(PROPERTY_NAME, PROPERTY_VALUE);
+
+ it.rsp.addResponse(details);
+ }
+ }
+}
diff --git a/solr/solr-ref-guide/modules/configuration-guide/pages/configuring-solr-xml.adoc b/solr/solr-ref-guide/modules/configuration-guide/pages/configuring-solr-xml.adoc
index 9de8707fe80..37de392bd36 100644
--- a/solr/solr-ref-guide/modules/configuration-guide/pages/configuring-solr-xml.adoc
+++ b/solr/solr-ref-guide/modules/configuration-guide/pages/configuring-solr-xml.adoc
@@ -107,20 +107,48 @@ For example, `<str name="adminHandler">com.myorg.MyAdminHandler</str>` would con
+
If this attribute isn't set, Solr uses the default admin handler, `org.apache.solr.handler.admin.CoreAdminHandler`.
-`collectionsHandler`::
+`coreAdminHandlerActions`::
+
[%autowidth,frame=none]
|===
|Optional |Default: none
|===
+
+This attribute does not need to be set.
++
+If defined, it should contain a list of custom actions to be registered within the CoreAdminHandler. Each entry of the list should be of `str` type where name of the entry defines the name of the action and value is a FQN (Fully qualified name) of an action class that inherits from `CoreAdminOp`.
++
+For example, actions can be defined like this:
++
+[source,xml]
+----
+ <coreAdminHandlerActions>
+ <str name="foo">com.example.FooAction</str>
+ <str name="bar">com.example.BarAction</str>
+ </coreAdminHandlerActions>
+----
++
+After defining custom actions they can be called using their names:
++
+[source,text]
+----
+http://localhost:8983/solr/admin/cores?action=foo
+----
+
+`collectionsHandler`::
++
+[%autowidth,frame=none]
+|===
+|Optional |Default: `org.apache.solr.handler.admin.CollectionsHandler`
+|===
++
As above, for custom CollectionsHandler implementations.
`infoHandler`::
+
[%autowidth,frame=none]
|===
-|Optional |Default: none
+|Optional |Default: `org.apache.solr.handler.admin.InfoHandler`
|===
+
As above, for custom InfoHandler implementations.