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:31:40 UTC

[solr] branch branch_9x 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 branch_9x
in repository https://gitbox.apache.org/repos/asf/solr.git


The following commit(s) were added to refs/heads/branch_9x by this push:
     new 0d91686db8f SOLR-16282: Update core admin handler to use pluggable custom actions (#931)
0d91686db8f is described below

commit 0d91686db8f492801f1945a5ea176618f2b4b455
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>
    (cherry picked from commit 1a70dcac5894db9652c6178a8bf7056d86d22f53)
---
 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 5aa5073be31..cd29080aec5 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -23,6 +23,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 798e07dcec1..a5b99edb38c 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;
@@ -110,6 +111,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;
@@ -829,6 +831,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.