You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@solr.apache.org by ge...@apache.org on 2022/08/04 20:56:31 UTC

[solr] branch branch_9x updated: SOLR-15736: Move configset logic into v2 APIs (#955)

This is an automated email from the ASF dual-hosted git repository.

gerlowskija 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 ee6e784b410 SOLR-15736: Move configset logic into v2 APIs (#955)
ee6e784b410 is described below

commit ee6e784b410907d98520a0621d6a945fdcd6f1c6
Author: Jason Gerlowski <ge...@apache.org>
AuthorDate: Thu Aug 4 16:52:11 2022 -0400

    SOLR-15736: Move configset logic into v2 APIs (#955)
---
 ...istributedCollectionConfigSetCommandRunner.java |   5 +-
 .../cloud/api/collections/OverseerStatusCmd.java   |   3 +-
 .../java/org/apache/solr/core/CoreContainer.java   |   2 +-
 .../java/org/apache/solr/handler/ClusterAPI.java   |  83 ----
 .../solr/handler/admin/ConfigSetsHandler.java      | 493 +++++----------------
 .../org/apache/solr/handler/api/ApiRegistrar.java  |  14 +
 .../solr/handler/configsets/ConfigSetAPIBase.java  | 209 +++++++++
 .../handler/configsets/CreateConfigSetAPI.java     |  88 ++++
 .../handler/configsets/DeleteConfigSetAPI.java     |  62 +++
 .../solr/handler/configsets/ListConfigSetsAPI.java |  49 ++
 .../handler/configsets/UploadConfigSetAPI.java     | 130 ++++++
 .../handler/configsets/UploadConfigSetFileAPI.java |  88 ++++
 .../solr/handler/configsets/package-info.java}     |  16 +-
 .../solr/request/DelegatingSolrQueryRequest.java   | 172 +++++++
 .../solr/handler/V2ClusterAPIMappingTest.java      |  67 ---
 .../apache/solr/handler/admin/TestConfigsApi.java  |  58 ---
 .../solrj/request/beans/CreateConfigPayload.java   |   5 +-
 17 files changed, 922 insertions(+), 622 deletions(-)

diff --git a/solr/core/src/java/org/apache/solr/cloud/api/collections/DistributedCollectionConfigSetCommandRunner.java b/solr/core/src/java/org/apache/solr/cloud/api/collections/DistributedCollectionConfigSetCommandRunner.java
index 32a0faaa749..07fe08f377b 100644
--- a/solr/core/src/java/org/apache/solr/cloud/api/collections/DistributedCollectionConfigSetCommandRunner.java
+++ b/solr/core/src/java/org/apache/solr/cloud/api/collections/DistributedCollectionConfigSetCommandRunner.java
@@ -52,7 +52,6 @@ import org.apache.solr.common.util.Pair;
 import org.apache.solr.common.util.SimpleOrderedMap;
 import org.apache.solr.common.util.SolrNamedThreadFactory;
 import org.apache.solr.core.CoreContainer;
-import org.apache.solr.handler.admin.ConfigSetsHandler;
 import org.apache.solr.logging.MDCLoggingContext;
 import org.apache.solr.response.SolrQueryResponse;
 import org.slf4j.Logger;
@@ -186,7 +185,7 @@ public class DistributedCollectionConfigSetCommandRunner {
    */
   public void runConfigSetCommand(
       SolrQueryResponse rsp,
-      ConfigSetsHandler.ConfigSetOperation operation,
+      ConfigSetParams.ConfigSetAction action,
       Map<String, Object> result,
       long timeoutMs)
       throws Exception {
@@ -198,8 +197,6 @@ public class DistributedCollectionConfigSetCommandRunner {
           "Solr is shutting down, no more Config Set API tasks may be executed");
     }
 
-    ConfigSetParams.ConfigSetAction action = operation.getAction();
-
     // never null
     String configSetName = (String) result.get(NAME);
     // baseConfigSetName will be null if we're not creating a new config set
diff --git a/solr/core/src/java/org/apache/solr/cloud/api/collections/OverseerStatusCmd.java b/solr/core/src/java/org/apache/solr/cloud/api/collections/OverseerStatusCmd.java
index 3cfe436878f..0b841fe8790 100644
--- a/solr/core/src/java/org/apache/solr/cloud/api/collections/OverseerStatusCmd.java
+++ b/solr/core/src/java/org/apache/solr/cloud/api/collections/OverseerStatusCmd.java
@@ -70,8 +70,7 @@ import org.slf4j.LoggerFactory;
  *       <ul>
  *         <li>{@code am_i_leader} (Overseer checking it is still the elected Overseer as it
  *             processes cluster state update messages)
- *         <li>{@code configset_}<i>{@code <config set operation>}</i> (from {@link
- *             org.apache.solr.handler.admin.ConfigSetsHandler.ConfigSetOperation})
+ *         <li>{@code configset_}<i>{@code <config set operation>}</i>
  *         <li>Cluster state change operation names from {@link
  *             org.apache.solr.common.params.CollectionParams.CollectionAction} (not all of them!)
  *             and {@link org.apache.solr.cloud.overseer.OverseerAction} (the complete list: {@code
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 1f56b151945..798e07dcec1 100644
--- a/solr/core/src/java/org/apache/solr/core/CoreContainer.java
+++ b/solr/core/src/java/org/apache/solr/core/CoreContainer.java
@@ -808,7 +808,7 @@ public class CoreContainer {
     ClusterAPI clusterAPI = new ClusterAPI(collectionsHandler, configSetsHandler);
     containerHandlers.getApiBag().registerObject(clusterAPI);
     containerHandlers.getApiBag().registerObject(clusterAPI.commands);
-    containerHandlers.getApiBag().registerObject(clusterAPI.configSetCommands);
+    ApiRegistrar.registerConfigsetApis(containerHandlers.getApiBag(), this);
 
     if (isZooKeeperAware()) {
       containerHandlers.getApiBag().registerObject(new SchemaDesignerAPI(this));
diff --git a/solr/core/src/java/org/apache/solr/handler/ClusterAPI.java b/solr/core/src/java/org/apache/solr/handler/ClusterAPI.java
index e58ed8c6bd3..ca3f6dd4139 100644
--- a/solr/core/src/java/org/apache/solr/handler/ClusterAPI.java
+++ b/solr/core/src/java/org/apache/solr/handler/ClusterAPI.java
@@ -20,7 +20,6 @@ package org.apache.solr.handler;
 import static org.apache.solr.client.solrj.SolrRequest.METHOD.DELETE;
 import static org.apache.solr.client.solrj.SolrRequest.METHOD.GET;
 import static org.apache.solr.client.solrj.SolrRequest.METHOD.POST;
-import static org.apache.solr.client.solrj.SolrRequest.METHOD.PUT;
 import static org.apache.solr.cloud.api.collections.CollectionHandlingUtils.REQUESTID;
 import static org.apache.solr.common.params.CollectionParams.ACTION;
 import static org.apache.solr.common.params.CollectionParams.CollectionAction.ADDROLE;
@@ -33,8 +32,6 @@ import static org.apache.solr.common.params.CollectionParams.CollectionAction.RE
 import static org.apache.solr.core.RateLimiterConfig.RL_CONFIG_KEY;
 import static org.apache.solr.security.PermissionNameProvider.Name.COLL_EDIT_PERM;
 import static org.apache.solr.security.PermissionNameProvider.Name.COLL_READ_PERM;
-import static org.apache.solr.security.PermissionNameProvider.Name.CONFIG_EDIT_PERM;
-import static org.apache.solr.security.PermissionNameProvider.Name.CONFIG_READ_PERM;
 
 import com.google.common.collect.Maps;
 import java.io.IOException;
@@ -49,16 +46,12 @@ import org.apache.solr.api.EndPoint;
 import org.apache.solr.api.PayloadObj;
 import org.apache.solr.client.solrj.cloud.DistribStateManager;
 import org.apache.solr.client.solrj.request.beans.ClusterPropPayload;
-import org.apache.solr.client.solrj.request.beans.CreateConfigPayload;
 import org.apache.solr.client.solrj.request.beans.RateLimiterPayload;
-import org.apache.solr.cloud.ConfigSetCmds;
 import org.apache.solr.common.SolrException;
 import org.apache.solr.common.annotation.JsonProperty;
 import org.apache.solr.common.cloud.ClusterProperties;
 import org.apache.solr.common.cloud.ZkStateReader;
 import org.apache.solr.common.params.CollectionParams;
-import org.apache.solr.common.params.CommonParams;
-import org.apache.solr.common.params.ConfigSetParams;
 import org.apache.solr.common.params.DefaultSolrParams;
 import org.apache.solr.common.params.ModifiableSolrParams;
 import org.apache.solr.common.util.ReflectMapWriter;
@@ -77,7 +70,6 @@ public class ClusterAPI {
   private final ConfigSetsHandler configSetsHandler;
 
   public final Commands commands = new Commands();
-  public final ConfigSetCommands configSetCommands = new ConfigSetCommands();
 
   public ClusterAPI(CollectionsHandler ch, ConfigSetsHandler configSetsHandler) {
     this.collectionsHandler = ch;
@@ -235,81 +227,6 @@ public class ClusterAPI {
     CollectionsHandler.CollectionOperation.DELETESTATUS_OP.execute(req, rsp, collectionsHandler);
   }
 
-  @EndPoint(method = DELETE, path = "/cluster/configs/{name}", permission = CONFIG_EDIT_PERM)
-  public void deleteConfigSet(SolrQueryRequest req, SolrQueryResponse rsp) throws Exception {
-    req =
-        wrapParams(
-            req,
-            "action",
-            ConfigSetParams.ConfigSetAction.DELETE.toString(),
-            CommonParams.NAME,
-            req.getPathTemplateValues().get("name"));
-    configSetsHandler.handleRequestBody(req, rsp);
-  }
-
-  @EndPoint(method = GET, path = "/cluster/configs", permission = CONFIG_READ_PERM)
-  public void listConfigSet(SolrQueryRequest req, SolrQueryResponse rsp) throws Exception {
-    req = wrapParams(req, "action", ConfigSetParams.ConfigSetAction.LIST.toString());
-    configSetsHandler.handleRequestBody(req, rsp);
-  }
-
-  @EndPoint(method = POST, path = "/cluster/configs", permission = CONFIG_EDIT_PERM)
-  public class ConfigSetCommands {
-
-    @Command(name = "create")
-    @SuppressWarnings("unchecked")
-    public void create(PayloadObj<CreateConfigPayload> obj) throws Exception {
-      Map<String, Object> mapVals = obj.get().toMap(new HashMap<>());
-      Map<String, Object> customProps = obj.get().properties;
-      if (customProps != null) {
-        customProps.forEach((k, o) -> mapVals.put(ConfigSetCmds.CONFIG_SET_PROPERTY_PREFIX + k, o));
-      }
-      mapVals.put("action", ConfigSetParams.ConfigSetAction.CREATE.toString());
-      configSetsHandler.handleRequestBody(wrapParams(obj.getRequest(), mapVals), obj.getResponse());
-    }
-  }
-
-  @EndPoint(method = PUT, path = "/cluster/configs/{name}", permission = CONFIG_EDIT_PERM)
-  public void uploadConfigSet(SolrQueryRequest req, SolrQueryResponse rsp) throws Exception {
-    req =
-        wrapParams(
-            req,
-            "action",
-            ConfigSetParams.ConfigSetAction.UPLOAD.toString(),
-            CommonParams.NAME,
-            req.getPathTemplateValues().get("name"),
-            ConfigSetParams.OVERWRITE,
-            true,
-            ConfigSetParams.CLEANUP,
-            false);
-    configSetsHandler.handleRequestBody(req, rsp);
-  }
-
-  @EndPoint(method = PUT, path = "/cluster/configs/{name}/*", permission = CONFIG_EDIT_PERM)
-  public void insertIntoConfigSet(SolrQueryRequest req, SolrQueryResponse rsp) throws Exception {
-    String path = req.getPathTemplateValues().get("*");
-    if (path == null || path.isBlank()) {
-      throw new SolrException(
-          SolrException.ErrorCode.BAD_REQUEST,
-          "In order to insert a file in a configSet, a filePath must be provided in the url after the name of the configSet.");
-    }
-    req =
-        wrapParams(
-            req,
-            Map.of(
-                "action",
-                ConfigSetParams.ConfigSetAction.UPLOAD.toString(),
-                CommonParams.NAME,
-                req.getPathTemplateValues().get("name"),
-                ConfigSetParams.FILE_PATH,
-                path,
-                ConfigSetParams.OVERWRITE,
-                true,
-                ConfigSetParams.CLEANUP,
-                false));
-    configSetsHandler.handleRequestBody(req, rsp);
-  }
-
   public static SolrQueryRequest wrapParams(SolrQueryRequest req, Object... def) {
     Map<String, Object> m = Utils.makeMap(def);
     return wrapParams(req, m);
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/ConfigSetsHandler.java b/solr/core/src/java/org/apache/solr/handler/admin/ConfigSetsHandler.java
index bba86c0cbd3..737f15a5fc6 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/ConfigSetsHandler.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/ConfigSetsHandler.java
@@ -16,66 +16,47 @@
  */
 package org.apache.solr.handler.admin;
 
-import static org.apache.solr.cloud.Overseer.QUEUE_OPERATION;
-import static org.apache.solr.cloud.OverseerConfigSetMessageHandler.CONFIGSETS_ACTION_PREFIX;
 import static org.apache.solr.common.params.CommonParams.NAME;
-import static org.apache.solr.common.params.ConfigSetParams.ConfigSetAction.CREATE;
-import static org.apache.solr.common.params.ConfigSetParams.ConfigSetAction.DELETE;
-import static org.apache.solr.common.params.ConfigSetParams.ConfigSetAction.LIST;
-import static org.apache.solr.common.params.ConfigSetParams.ConfigSetAction.UPLOAD;
+import static org.apache.solr.handler.configsets.UploadConfigSetFileAPI.FILEPATH_PLACEHOLDER;
 
-import java.io.IOException;
-import java.io.InputStream;
+import com.google.common.collect.Maps;
 import java.lang.invoke.MethodHandles;
-import java.nio.charset.StandardCharsets;
-import java.util.Collections;
-import java.util.Iterator;
-import java.util.List;
+import java.util.HashMap;
 import java.util.Map;
-import java.util.Optional;
-import java.util.concurrent.TimeUnit;
-import java.util.zip.ZipEntry;
-import java.util.zip.ZipInputStream;
-import org.apache.commons.io.IOUtils;
-import org.apache.commons.lang3.StringUtils;
-import org.apache.solr.client.solrj.SolrResponse;
+import org.apache.solr.api.PayloadObj;
+import org.apache.solr.client.solrj.request.beans.CreateConfigPayload;
 import org.apache.solr.cloud.ConfigSetCmds;
-import org.apache.solr.cloud.OverseerSolrResponse;
-import org.apache.solr.cloud.OverseerSolrResponseSerializer;
-import org.apache.solr.cloud.OverseerTaskQueue.QueueEvent;
-import org.apache.solr.cloud.api.collections.DistributedCollectionConfigSetCommandRunner;
 import org.apache.solr.common.SolrException;
 import org.apache.solr.common.SolrException.ErrorCode;
-import org.apache.solr.common.cloud.ZkNodeProps;
 import org.apache.solr.common.params.ConfigSetParams;
 import org.apache.solr.common.params.ConfigSetParams.ConfigSetAction;
+import org.apache.solr.common.params.DefaultSolrParams;
+import org.apache.solr.common.params.ModifiableSolrParams;
 import org.apache.solr.common.params.SolrParams;
-import org.apache.solr.common.util.ContentStream;
-import org.apache.solr.common.util.NamedList;
-import org.apache.solr.common.util.SimpleOrderedMap;
-import org.apache.solr.common.util.Utils;
-import org.apache.solr.core.ConfigSetService;
 import org.apache.solr.core.CoreContainer;
 import org.apache.solr.handler.RequestHandlerBase;
+import org.apache.solr.handler.configsets.CreateConfigSetAPI;
+import org.apache.solr.handler.configsets.DeleteConfigSetAPI;
+import org.apache.solr.handler.configsets.ListConfigSetsAPI;
+import org.apache.solr.handler.configsets.UploadConfigSetAPI;
+import org.apache.solr.handler.configsets.UploadConfigSetFileAPI;
+import org.apache.solr.request.DelegatingSolrQueryRequest;
 import org.apache.solr.request.SolrQueryRequest;
 import org.apache.solr.response.SolrQueryResponse;
-import org.apache.solr.security.AuthenticationPlugin;
 import org.apache.solr.security.AuthorizationContext;
 import org.apache.solr.security.PermissionNameProvider;
-import org.apache.zookeeper.KeeperException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 /** A {@link org.apache.solr.request.SolrRequestHandler} for ConfigSets API requests. */
 public class ConfigSetsHandler extends RequestHandlerBase implements PermissionNameProvider {
+  // TODO refactor into o.a.s.handler.configsets package to live alongside actual API logic
   public static final Boolean DISABLE_CREATE_AUTH_CHECKS =
       Boolean.getBoolean("solr.disableConfigSetsCreateAuthChecks"); // this is for back compat only
   public static final String DEFAULT_CONFIGSET_NAME = "_default";
   public static final String AUTOCREATED_CONFIGSET_SUFFIX = ".AUTOCREATED";
   private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
   protected final CoreContainer coreContainer;
-  private final Optional<DistributedCollectionConfigSetCommandRunner>
-      distributedCollectionConfigSetCommandRunner;
   public static long CONFIG_SET_TIMEOUT = 300 * 1000;
   /**
    * Overloaded ctor to inject CoreContainer into the handler.
@@ -84,10 +65,6 @@ public class ConfigSetsHandler extends RequestHandlerBase implements PermissionN
    */
   public ConfigSetsHandler(final CoreContainer coreContainer) {
     this.coreContainer = coreContainer;
-    distributedCollectionConfigSetCommandRunner =
-        coreContainer != null
-            ? coreContainer.getDistributedCollectionCommandRunner()
-            : Optional.empty();
   }
 
   public static String getSuffixedNameForAutoGeneratedConfigSet(String configName) {
@@ -103,21 +80,93 @@ public class ConfigSetsHandler extends RequestHandlerBase implements PermissionN
     checkErrors();
 
     // Pick the action
-    SolrParams params = req.getParams();
-    String a = params.get(ConfigSetParams.ACTION);
-    if (a != null) {
-      ConfigSetAction action = ConfigSetAction.get(a);
-      if (action == null)
-        throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Unknown action: " + a);
-      if (action == ConfigSetAction.UPLOAD) {
-        handleConfigUploadRequest(req, rsp);
-        return;
-      }
-      invokeAction(req, rsp, action);
-    } else {
-      throw new SolrException(ErrorCode.BAD_REQUEST, "action is a required param");
-    }
+    final SolrParams requiredSolrParams = req.getParams().required();
+    final String actionStr = requiredSolrParams.get(ConfigSetParams.ACTION);
+    ConfigSetAction action = ConfigSetAction.get(actionStr);
+    if (action == null) {
+      throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Unknown action: " + actionStr);
+    }
+
+    switch (action) {
+      case DELETE:
+        final DeleteConfigSetAPI deleteConfigSetAPI = new DeleteConfigSetAPI(coreContainer);
+        final SolrQueryRequest v2DeleteReq =
+            new DelegatingSolrQueryRequest(req) {
+              @Override
+              public Map<String, String> getPathTemplateValues() {
+                return Map.of(
+                    DeleteConfigSetAPI.CONFIGSET_NAME_PLACEHOLDER,
+                    req.getParams().required().get(NAME));
+              }
+            };
+        deleteConfigSetAPI.deleteConfigSet(v2DeleteReq, rsp);
+        break;
+      case UPLOAD:
+        final SolrQueryRequest v2UploadReq =
+            new DelegatingSolrQueryRequest(req) {
+              @Override
+              public Map<String, String> getPathTemplateValues() {
+                final Map<String, String> templateValsByName = Maps.newHashMap();
+
+                templateValsByName.put(
+                    UploadConfigSetAPI.CONFIGSET_NAME_PLACEHOLDER,
+                    req.getParams().required().get(NAME));
+                if (!req.getParams().get(ConfigSetParams.FILE_PATH, "").isEmpty()) {
+                  templateValsByName.put(
+                      FILEPATH_PLACEHOLDER, req.getParams().get(ConfigSetParams.FILE_PATH));
+                }
+                return templateValsByName;
+              }
+
+              // Set the v1 default vals where they differ from v2's
+              @Override
+              public SolrParams getParams() {
+                final ModifiableSolrParams v1Defaults = new ModifiableSolrParams();
+                v1Defaults.add(ConfigSetParams.OVERWRITE, "false");
+                v1Defaults.add(ConfigSetParams.CLEANUP, "false");
+                return new DefaultSolrParams(super.getParams(), v1Defaults);
+              }
+            };
+        if (req.getParams()
+            .get(ConfigSetParams.FILE_PATH, "")
+            .isEmpty()) { // Uploading a whole configset
+          new UploadConfigSetAPI(coreContainer).uploadConfigSet(v2UploadReq, rsp);
+        } else { // Uploading a single file
+          new UploadConfigSetFileAPI(coreContainer).updateConfigSetFile(v2UploadReq, rsp);
+        }
+        break;
+      case LIST:
+        new ListConfigSetsAPI(coreContainer).listConfigSet(req, rsp);
+        break;
+      case CREATE:
+        final String newConfigSetName = req.getParams().get(NAME);
+        if (newConfigSetName == null || newConfigSetName.length() == 0) {
+          throw new SolrException(ErrorCode.BAD_REQUEST, "ConfigSet name not specified");
+        }
 
+        // Map v1 parameters into v2 format and process request
+        final CreateConfigPayload createPayload = new CreateConfigPayload();
+        createPayload.name = newConfigSetName;
+        if (req.getParams().get(ConfigSetCmds.BASE_CONFIGSET) != null) {
+          createPayload.baseConfigSet = req.getParams().get(ConfigSetCmds.BASE_CONFIGSET);
+        }
+        createPayload.properties = new HashMap<>();
+        req.getParams().stream()
+            .filter(entry -> entry.getKey().startsWith(ConfigSetCmds.CONFIG_SET_PROPERTY_PREFIX))
+            .forEach(
+                entry -> {
+                  final String newKey =
+                      entry.getKey().substring(ConfigSetCmds.CONFIG_SET_PROPERTY_PREFIX.length());
+                  final Object value =
+                      (entry.getValue().length == 1) ? entry.getValue()[0] : entry.getValue();
+                  createPayload.properties.put(newKey, value);
+                });
+        final CreateConfigSetAPI createConfigSetAPI = new CreateConfigSetAPI(coreContainer);
+        createConfigSetAPI.create(new PayloadObj<>("create", null, createPayload, req, rsp));
+        break;
+      default:
+        throw new IllegalStateException("Unexpected ConfigSetAction detected: " + action);
+    }
     rsp.setHttpCaching(false);
   }
 
@@ -133,261 +182,6 @@ public class ConfigSetsHandler extends RequestHandlerBase implements PermissionN
     }
   }
 
-  void invokeAction(SolrQueryRequest req, SolrQueryResponse rsp, ConfigSetAction action)
-      throws Exception {
-    ConfigSetOperation operation = ConfigSetOperation.get(action);
-    if (log.isInfoEnabled()) {
-      log.info(
-          "Invoked ConfigSet Action :{} with params {} ", action.toLower(), req.getParamString());
-    }
-    Map<String, Object> result = operation.call(req, rsp, this);
-    if (result != null) {
-      if (distributedCollectionConfigSetCommandRunner.isPresent()) {
-        distributedCollectionConfigSetCommandRunner
-            .get()
-            .runConfigSetCommand(rsp, operation, result, CONFIG_SET_TIMEOUT);
-      } else { // Sending the Collection API message to Overseer via a Zookeeper queue
-        sendToOverseer(rsp, operation, result);
-      }
-    }
-  }
-
-  protected void sendToOverseer(
-      SolrQueryResponse rsp, ConfigSetOperation operation, Map<String, Object> result)
-      throws KeeperException, InterruptedException {
-    // We need to differentiate between collection and configsets actions since they currently
-    // use the same underlying queue.
-    result.put(QUEUE_OPERATION, CONFIGSETS_ACTION_PREFIX + operation.action.toLower());
-    ZkNodeProps props = new ZkNodeProps(result);
-    handleResponse(operation.action.toLower(), props, rsp, CONFIG_SET_TIMEOUT);
-  }
-
-  private void handleConfigUploadRequest(SolrQueryRequest req, SolrQueryResponse rsp)
-      throws Exception {
-    if (!"true".equals(System.getProperty("configset.upload.enabled", "true"))) {
-      throw new SolrException(
-          ErrorCode.BAD_REQUEST,
-          "Configset upload feature is disabled. To enable this, start Solr with '-Dconfigset.upload.enabled=true'.");
-    }
-
-    String configSetName = req.getParams().get(NAME);
-    if (StringUtils.isBlank(configSetName)) {
-      throw new SolrException(
-          ErrorCode.BAD_REQUEST,
-          "The configuration name should be provided in the \"name\" parameter");
-    }
-
-    ConfigSetService configSetService = coreContainer.getConfigSetService();
-
-    boolean overwritesExisting = configSetService.checkConfigExists(configSetName);
-
-    boolean requestIsTrusted = isTrusted(req, coreContainer.getAuthenticationPlugin());
-
-    // Get upload parameters
-    String singleFilePath = req.getParams().get(ConfigSetParams.FILE_PATH, "");
-    boolean allowOverwrite = req.getParams().getBool(ConfigSetParams.OVERWRITE, false);
-    boolean cleanup = req.getParams().getBool(ConfigSetParams.CLEANUP, false);
-
-    Iterator<ContentStream> contentStreamsIterator = req.getContentStreams().iterator();
-
-    if (!contentStreamsIterator.hasNext()) {
-      throw new SolrException(
-          ErrorCode.BAD_REQUEST, "No stream found for the config data to be uploaded");
-    }
-
-    InputStream inputStream = contentStreamsIterator.next().getStream();
-
-    // Only Upload a single file
-    if (!singleFilePath.isEmpty()) {
-      String fixedSingleFilePath = singleFilePath;
-      if (fixedSingleFilePath.charAt(0) == '/') {
-        fixedSingleFilePath = fixedSingleFilePath.substring(1);
-      }
-      if (fixedSingleFilePath.isEmpty()) {
-        throw new SolrException(
-            ErrorCode.BAD_REQUEST,
-            "The file path provided for upload, '" + singleFilePath + "', is not valid.");
-      } else if (cleanup) {
-        // Cleanup is not allowed while using singleFilePath upload
-        throw new SolrException(
-            ErrorCode.BAD_REQUEST,
-            "ConfigSet uploads do not allow cleanup=true when file path is used.");
-      } else {
-        // Create a node for the configuration in config
-        // For creating the baseNode, the cleanup parameter is only allowed to be true when
-        // singleFilePath is not passed.
-        createBaseNode(configSetService, overwritesExisting, requestIsTrusted, configSetName);
-        configSetService.uploadFileToConfig(
-            configSetName, fixedSingleFilePath, IOUtils.toByteArray(inputStream), allowOverwrite);
-      }
-      return;
-    }
-
-    if (overwritesExisting && !allowOverwrite) {
-      throw new SolrException(
-          ErrorCode.BAD_REQUEST,
-          "The configuration " + configSetName + " already exists in zookeeper");
-    }
-
-    List<String> filesToDelete;
-    if (overwritesExisting && cleanup) {
-      filesToDelete = configSetService.getAllConfigFiles(configSetName);
-    } else {
-      filesToDelete = Collections.emptyList();
-    }
-
-    // Create a node for the configuration in zookeeper
-    // For creating the baseZnode, the cleanup parameter is only allowed to be true when
-    // singleFilePath is not passed.
-    createBaseNode(configSetService, overwritesExisting, requestIsTrusted, configSetName);
-
-    try (ZipInputStream zis = new ZipInputStream(inputStream, StandardCharsets.UTF_8)) {
-      boolean hasEntry = false;
-      ZipEntry zipEntry;
-      while ((zipEntry = zis.getNextEntry()) != null) {
-        hasEntry = true;
-        String filePath = zipEntry.getName();
-        filesToDelete.remove(filePath);
-        if (!zipEntry.isDirectory()) {
-          configSetService.uploadFileToConfig(
-              configSetName, filePath, IOUtils.toByteArray(zis), true);
-        }
-      }
-      if (!hasEntry) {
-        throw new SolrException(
-            ErrorCode.BAD_REQUEST,
-            "Either empty zipped data, or non-zipped data was uploaded. In order to upload a configSet, you must zip a non-empty directory to upload.");
-      }
-    }
-    deleteUnusedFiles(configSetService, configSetName, filesToDelete);
-
-    // If the request is doing a full trusted overwrite of an untrusted configSet (overwrite=true,
-    // cleanup=true), then trust the configSet.
-    if (cleanup
-        && requestIsTrusted
-        && overwritesExisting
-        && !isCurrentlyTrusted(configSetService, configSetName)) {
-      Map<String, Object> metadata = Collections.singletonMap("trusted", true);
-      configSetService.setConfigMetadata(configSetName, metadata);
-    }
-  }
-
-  private void createBaseNode(
-      ConfigSetService configSetService,
-      boolean overwritesExisting,
-      boolean requestIsTrusted,
-      String configName)
-      throws IOException {
-    Map<String, Object> metadata = Collections.singletonMap("trusted", requestIsTrusted);
-
-    if (overwritesExisting) {
-      if (!requestIsTrusted) {
-        ensureOverwritingUntrustedConfigSet(configSetService, configName);
-      }
-      // If the request is trusted and cleanup=true, then the configSet will be set to trusted after
-      // the overwriting has been done.
-    } else {
-      configSetService.setConfigMetadata(configName, metadata);
-    }
-  }
-
-  private void deleteUnusedFiles(
-      ConfigSetService configSetService, String configName, List<String> filesToDelete)
-      throws IOException {
-    if (!filesToDelete.isEmpty()) {
-      if (log.isInfoEnabled()) {
-        log.info("Cleaning up {} unused files", filesToDelete.size());
-      }
-      if (log.isDebugEnabled()) {
-        log.debug("Cleaning up unused files: {}", filesToDelete);
-      }
-      configSetService.deleteFilesFromConfig(configName, filesToDelete);
-    }
-  }
-
-  /*
-   * Fail if an untrusted request tries to update a trusted ConfigSet
-   */
-  private void ensureOverwritingUntrustedConfigSet(
-      ConfigSetService configSetService, String configName) throws IOException {
-    boolean isCurrentlyTrusted = isCurrentlyTrusted(configSetService, configName);
-    if (isCurrentlyTrusted) {
-      throw new SolrException(
-          ErrorCode.BAD_REQUEST,
-          "Trying to make an untrusted ConfigSet update on a trusted configSet");
-    }
-  }
-
-  private static boolean isCurrentlyTrusted(ConfigSetService configSetService, String configName)
-      throws IOException {
-    Map<String, Object> contentMap = configSetService.getConfigMetadata(configName);
-    return (boolean) contentMap.getOrDefault("trusted", true);
-  }
-
-  static boolean isTrusted(SolrQueryRequest req, AuthenticationPlugin authPlugin) {
-    if (authPlugin != null && req.getUserPrincipal() != null) {
-      log.debug("Trusted configset request");
-      return true;
-    }
-    log.debug("Untrusted configset request");
-    return false;
-  }
-
-  private void handleResponse(String operation, ZkNodeProps m, SolrQueryResponse rsp, long timeout)
-      throws KeeperException, InterruptedException {
-    long time = System.nanoTime();
-
-    QueueEvent event =
-        coreContainer.getZkController().getOverseerConfigSetQueue().offer(Utils.toJSON(m), timeout);
-    if (event.getBytes() != null) {
-      SolrResponse response = OverseerSolrResponseSerializer.deserialize(event.getBytes());
-      rsp.getValues().addAll(response.getResponse());
-      SimpleOrderedMap<?> exp = (SimpleOrderedMap<?>) response.getResponse().get("exception");
-      if (exp != null) {
-        Integer code = (Integer) exp.get("rspCode");
-        rsp.setException(
-            new SolrException(
-                code != null && code != -1 ? ErrorCode.getErrorCode(code) : ErrorCode.SERVER_ERROR,
-                (String) exp.get("msg")));
-      }
-    } else {
-      if (System.nanoTime() - time
-          >= TimeUnit.NANOSECONDS.convert(timeout, TimeUnit.MILLISECONDS)) {
-        throw new SolrException(
-            ErrorCode.SERVER_ERROR, operation + " the configset time out:" + timeout / 1000 + "s");
-      } else if (event.getWatchedEvent() != null) {
-        throw new SolrException(
-            ErrorCode.SERVER_ERROR,
-            operation
-                + " the configset error [Watcher fired on path: "
-                + event.getWatchedEvent().getPath()
-                + " state: "
-                + event.getWatchedEvent().getState()
-                + " type "
-                + event.getWatchedEvent().getType()
-                + "]");
-      } else {
-        throw new SolrException(ErrorCode.SERVER_ERROR, operation + " the configset unknown case");
-      }
-    }
-  }
-
-  private static Map<String, Object> copyPropertiesWithPrefix(
-      SolrParams params, Map<String, Object> props, String prefix) {
-    Iterator<String> iter = params.getParameterNamesIterator();
-    while (iter.hasNext()) {
-      String param = iter.next();
-      if (param.startsWith(prefix)) {
-        props.put(param, params.get(param));
-      }
-    }
-
-    // The configset created via an API should be mutable.
-    props.put("immutable", "false");
-
-    return props;
-  }
-
   @Override
   public String getDescription() {
     return "Manage SolrCloud ConfigSets";
@@ -398,93 +192,6 @@ public class ConfigSetsHandler extends RequestHandlerBase implements PermissionN
     return Category.ADMIN;
   }
 
-  public enum ConfigSetOperation {
-    UPLOAD_OP(UPLOAD) {
-      @Override
-      public Map<String, Object> call(
-          SolrQueryRequest req, SolrQueryResponse rsp, ConfigSetsHandler h) throws Exception {
-        h.handleConfigUploadRequest(req, rsp);
-        return null;
-      }
-    },
-    CREATE_OP(CREATE) {
-      @Override
-      public Map<String, Object> call(
-          SolrQueryRequest req, SolrQueryResponse rsp, ConfigSetsHandler h) throws Exception {
-        String baseConfigSetName =
-            req.getParams().get(ConfigSetCmds.BASE_CONFIGSET, DEFAULT_CONFIGSET_NAME);
-        String newConfigSetName = req.getParams().get(NAME);
-        if (newConfigSetName == null || newConfigSetName.length() == 0) {
-          throw new SolrException(ErrorCode.BAD_REQUEST, "ConfigSet name not specified");
-        }
-
-        if (h.coreContainer.getConfigSetService().checkConfigExists(newConfigSetName)) {
-          throw new SolrException(
-              ErrorCode.BAD_REQUEST, "ConfigSet already exists: " + newConfigSetName);
-        }
-
-        // is there a base config that already exists
-        if (!h.coreContainer.getConfigSetService().checkConfigExists(baseConfigSetName)) {
-          throw new SolrException(
-              ErrorCode.BAD_REQUEST, "Base ConfigSet does not exist: " + baseConfigSetName);
-        }
-
-        Map<String, Object> props = CollectionsHandler.copy(req.getParams().required(), null, NAME);
-        props.put(ConfigSetCmds.BASE_CONFIGSET, baseConfigSetName);
-        if (!DISABLE_CREATE_AUTH_CHECKS
-            && !isTrusted(req, h.coreContainer.getAuthenticationPlugin())
-            && isCurrentlyTrusted(h.coreContainer.getConfigSetService(), baseConfigSetName)) {
-          throw new SolrException(
-              ErrorCode.UNAUTHORIZED,
-              "Can't create a configset with an unauthenticated request from a trusted "
-                  + ConfigSetCmds.BASE_CONFIGSET);
-        }
-        return copyPropertiesWithPrefix(
-            req.getParams(), props, ConfigSetCmds.CONFIG_SET_PROPERTY_PREFIX);
-      }
-    },
-    DELETE_OP(DELETE) {
-      @Override
-      public Map<String, Object> call(
-          SolrQueryRequest req, SolrQueryResponse rsp, ConfigSetsHandler h) throws Exception {
-        return CollectionsHandler.copy(req.getParams().required(), null, NAME);
-      }
-    },
-    @SuppressWarnings({"unchecked"})
-    LIST_OP(LIST) {
-      @Override
-      public Map<String, Object> call(
-          SolrQueryRequest req, SolrQueryResponse rsp, ConfigSetsHandler h) throws Exception {
-        NamedList<Object> results = new NamedList<>();
-        List<String> configSetsList = h.coreContainer.getConfigSetService().listConfigs();
-        results.add("configSets", configSetsList);
-        SolrResponse response = new OverseerSolrResponse(results);
-        rsp.getValues().addAll(response.getResponse());
-        return null;
-      }
-    };
-
-    ConfigSetAction action;
-
-    ConfigSetOperation(ConfigSetAction action) {
-      this.action = action;
-    }
-
-    public ConfigSetAction getAction() {
-      return action;
-    }
-
-    public abstract Map<String, Object> call(
-        SolrQueryRequest req, SolrQueryResponse rsp, ConfigSetsHandler h) throws Exception;
-
-    public static ConfigSetOperation get(ConfigSetAction action) {
-      for (ConfigSetOperation op : values()) {
-        if (op.action == action) return op;
-      }
-      throw new SolrException(ErrorCode.SERVER_ERROR, "No such action" + action);
-    }
-  }
-
   @Override
   public Name getPermissionName(AuthorizationContext ctx) {
     String a = ctx.getParams().get(ConfigSetParams.ACTION);
diff --git a/solr/core/src/java/org/apache/solr/handler/api/ApiRegistrar.java b/solr/core/src/java/org/apache/solr/handler/api/ApiRegistrar.java
index a0de011d3ca..0fdbe40122d 100644
--- a/solr/core/src/java/org/apache/solr/handler/api/ApiRegistrar.java
+++ b/solr/core/src/java/org/apache/solr/handler/api/ApiRegistrar.java
@@ -18,6 +18,7 @@
 package org.apache.solr.handler.api;
 
 import org.apache.solr.api.ApiBag;
+import org.apache.solr.core.CoreContainer;
 import org.apache.solr.handler.admin.CollectionsHandler;
 import org.apache.solr.handler.admin.api.AddReplicaAPI;
 import org.apache.solr.handler.admin.api.AddReplicaPropertyAPI;
@@ -37,6 +38,11 @@ import org.apache.solr.handler.admin.api.ReloadCollectionAPI;
 import org.apache.solr.handler.admin.api.SetCollectionPropertyAPI;
 import org.apache.solr.handler.admin.api.SplitShardAPI;
 import org.apache.solr.handler.admin.api.SyncShardAPI;
+import org.apache.solr.handler.configsets.CreateConfigSetAPI;
+import org.apache.solr.handler.configsets.DeleteConfigSetAPI;
+import org.apache.solr.handler.configsets.ListConfigSetsAPI;
+import org.apache.solr.handler.configsets.UploadConfigSetAPI;
+import org.apache.solr.handler.configsets.UploadConfigSetFileAPI;
 
 /**
  * Registers annotation-based V2 APIs with an {@link ApiBag}
@@ -73,4 +79,12 @@ public class ApiRegistrar {
     // here for simplicity.
     apiBag.registerObject(new DeleteReplicaAPI(collectionsHandler));
   }
+
+  public static void registerConfigsetApis(ApiBag apiBag, CoreContainer container) {
+    apiBag.registerObject(new CreateConfigSetAPI(container));
+    apiBag.registerObject(new DeleteConfigSetAPI(container));
+    apiBag.registerObject(new ListConfigSetsAPI(container));
+    apiBag.registerObject(new UploadConfigSetAPI(container));
+    apiBag.registerObject(new UploadConfigSetFileAPI(container));
+  }
 }
diff --git a/solr/core/src/java/org/apache/solr/handler/configsets/ConfigSetAPIBase.java b/solr/core/src/java/org/apache/solr/handler/configsets/ConfigSetAPIBase.java
new file mode 100644
index 00000000000..c4eff442496
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/handler/configsets/ConfigSetAPIBase.java
@@ -0,0 +1,209 @@
+/*
+ * 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.configsets;
+
+import static org.apache.solr.cloud.Overseer.QUEUE_OPERATION;
+import static org.apache.solr.cloud.OverseerConfigSetMessageHandler.CONFIGSETS_ACTION_PREFIX;
+import static org.apache.solr.handler.admin.ConfigSetsHandler.CONFIG_SET_TIMEOUT;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.invoke.MethodHandles;
+import java.security.Principal;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+import org.apache.solr.client.solrj.SolrResponse;
+import org.apache.solr.cloud.OverseerSolrResponseSerializer;
+import org.apache.solr.cloud.OverseerTaskQueue;
+import org.apache.solr.cloud.api.collections.DistributedCollectionConfigSetCommandRunner;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.cloud.ZkNodeProps;
+import org.apache.solr.common.params.ConfigSetParams;
+import org.apache.solr.common.util.ContentStream;
+import org.apache.solr.common.util.SimpleOrderedMap;
+import org.apache.solr.common.util.Utils;
+import org.apache.solr.core.ConfigSetService;
+import org.apache.solr.core.CoreContainer;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.response.SolrQueryResponse;
+import org.apache.solr.security.AuthenticationPlugin;
+import org.apache.zookeeper.KeeperException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Parent class for all APIs that manipulate configsets
+ *
+ * <p>Contains utilities for tasks common in configset manipulation, including running configset
+ * "commands" and checking configset "trusted-ness".
+ */
+public class ConfigSetAPIBase {
+
+  private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+  protected final CoreContainer coreContainer;
+  protected final Optional<DistributedCollectionConfigSetCommandRunner>
+      distributedCollectionConfigSetCommandRunner;
+
+  protected final ConfigSetService configSetService;
+
+  public ConfigSetAPIBase(CoreContainer coreContainer) {
+    this.coreContainer = coreContainer;
+    this.distributedCollectionConfigSetCommandRunner =
+        coreContainer.getDistributedCollectionCommandRunner();
+    this.configSetService = coreContainer.getConfigSetService();
+  }
+
+  protected void runConfigSetCommand(
+      SolrQueryResponse rsp,
+      ConfigSetParams.ConfigSetAction action,
+      Map<String, Object> messageToSend)
+      throws Exception {
+    if (log.isInfoEnabled()) {
+      log.info("Invoked ConfigSet Action :{} with params {} ", action.toLower(), messageToSend);
+    }
+
+    if (distributedCollectionConfigSetCommandRunner.isPresent()) {
+      distributedCollectionConfigSetCommandRunner
+          .get()
+          .runConfigSetCommand(rsp, action, messageToSend, CONFIG_SET_TIMEOUT);
+    } else {
+      sendToOverseer(rsp, action, messageToSend);
+    }
+  }
+
+  protected void ensureConfigSetUploadEnabled() {
+    if (!"true".equals(System.getProperty("configset.upload.enabled", "true"))) {
+      throw new SolrException(
+          SolrException.ErrorCode.BAD_REQUEST,
+          "Configset upload feature is disabled. To enable this, start Solr with '-Dconfigset.upload.enabled=true'.");
+    }
+  }
+
+  protected InputStream ensureNonEmptyInputStream(SolrQueryRequest req) throws IOException {
+    Iterator<ContentStream> contentStreamsIterator = req.getContentStreams().iterator();
+
+    if (!contentStreamsIterator.hasNext()) {
+      throw new SolrException(
+          SolrException.ErrorCode.BAD_REQUEST,
+          "No stream found for the config data to be uploaded");
+    }
+
+    return contentStreamsIterator.next().getStream();
+  }
+
+  protected boolean isTrusted(Principal userPrincipal, AuthenticationPlugin authPlugin) {
+    if (authPlugin != null && userPrincipal != null) {
+      log.debug("Trusted configset request");
+      return true;
+    }
+    log.debug("Untrusted configset request");
+    return false;
+  }
+
+  protected boolean isCurrentlyTrusted(String configName) throws IOException {
+    Map<String, Object> contentMap = configSetService.getConfigMetadata(configName);
+    return (boolean) contentMap.getOrDefault("trusted", true);
+  }
+
+  protected void createBaseNode(
+      ConfigSetService configSetService,
+      boolean overwritesExisting,
+      boolean requestIsTrusted,
+      String configName)
+      throws IOException {
+    Map<String, Object> metadata = Collections.singletonMap("trusted", requestIsTrusted);
+
+    if (overwritesExisting) {
+      if (!requestIsTrusted) {
+        ensureOverwritingUntrustedConfigSet(configName);
+      }
+      // If the request is trusted and cleanup=true, then the configSet will be set to trusted after
+      // the overwriting has been done.
+    } else {
+      configSetService.setConfigMetadata(configName, metadata);
+    }
+  }
+
+  /*
+   * Fail if an untrusted request tries to update a trusted ConfigSet
+   */
+  private void ensureOverwritingUntrustedConfigSet(String configName) throws IOException {
+    boolean isCurrentlyTrusted = isCurrentlyTrusted(configName);
+    if (isCurrentlyTrusted) {
+      throw new SolrException(
+          SolrException.ErrorCode.BAD_REQUEST,
+          "Trying to make an untrusted ConfigSet update on a trusted configSet");
+    }
+  }
+
+  private void sendToOverseer(
+      SolrQueryResponse rsp, ConfigSetParams.ConfigSetAction action, Map<String, Object> result)
+      throws KeeperException, InterruptedException {
+    // We need to differentiate between collection and configsets actions since they currently
+    // use the same underlying queue.
+    result.put(QUEUE_OPERATION, CONFIGSETS_ACTION_PREFIX + action.toLower());
+    ZkNodeProps props = new ZkNodeProps(result);
+    handleResponse(action.toLower(), props, rsp, CONFIG_SET_TIMEOUT);
+  }
+
+  private void handleResponse(String operation, ZkNodeProps m, SolrQueryResponse rsp, long timeout)
+      throws KeeperException, InterruptedException {
+    long time = System.nanoTime();
+
+    OverseerTaskQueue.QueueEvent event =
+        coreContainer.getZkController().getOverseerConfigSetQueue().offer(Utils.toJSON(m), timeout);
+    if (event.getBytes() != null) {
+      SolrResponse response = OverseerSolrResponseSerializer.deserialize(event.getBytes());
+      rsp.getValues().addAll(response.getResponse());
+      SimpleOrderedMap<?> exp = (SimpleOrderedMap<?>) response.getResponse().get("exception");
+      if (exp != null) {
+        Integer code = (Integer) exp.get("rspCode");
+        rsp.setException(
+            new SolrException(
+                code != null && code != -1
+                    ? SolrException.ErrorCode.getErrorCode(code)
+                    : SolrException.ErrorCode.SERVER_ERROR,
+                (String) exp.get("msg")));
+      }
+    } else {
+      if (System.nanoTime() - time
+          >= TimeUnit.NANOSECONDS.convert(timeout, TimeUnit.MILLISECONDS)) {
+        throw new SolrException(
+            SolrException.ErrorCode.SERVER_ERROR,
+            operation + " the configset time out:" + timeout / 1000 + "s");
+      } else if (event.getWatchedEvent() != null) {
+        throw new SolrException(
+            SolrException.ErrorCode.SERVER_ERROR,
+            operation
+                + " the configset error [Watcher fired on path: "
+                + event.getWatchedEvent().getPath()
+                + " state: "
+                + event.getWatchedEvent().getState()
+                + " type "
+                + event.getWatchedEvent().getType()
+                + "]");
+      } else {
+        throw new SolrException(
+            SolrException.ErrorCode.SERVER_ERROR, operation + " the configset unknown case");
+      }
+    }
+  }
+}
diff --git a/solr/core/src/java/org/apache/solr/handler/configsets/CreateConfigSetAPI.java b/solr/core/src/java/org/apache/solr/handler/configsets/CreateConfigSetAPI.java
new file mode 100644
index 00000000000..7f52a06a525
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/handler/configsets/CreateConfigSetAPI.java
@@ -0,0 +1,88 @@
+/*
+ * 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.configsets;
+
+import static org.apache.solr.client.solrj.SolrRequest.METHOD.POST;
+import static org.apache.solr.common.params.CommonParams.NAME;
+import static org.apache.solr.handler.admin.ConfigSetsHandler.DISABLE_CREATE_AUTH_CHECKS;
+import static org.apache.solr.security.PermissionNameProvider.Name.CONFIG_EDIT_PERM;
+
+import java.util.HashMap;
+import java.util.Map;
+import org.apache.commons.collections4.MapUtils;
+import org.apache.solr.api.Command;
+import org.apache.solr.api.EndPoint;
+import org.apache.solr.api.PayloadObj;
+import org.apache.solr.client.solrj.request.beans.CreateConfigPayload;
+import org.apache.solr.cloud.ConfigSetCmds;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.params.ConfigSetParams;
+import org.apache.solr.core.CoreContainer;
+
+/**
+ * V2 API for creating a new configset as a copy of an existing one.
+ *
+ * <p>This API (POST /v2/cluster/configs {"create": {...}}) is analogous to the v1
+ * /admin/configs?action=CREATE command.
+ */
+@EndPoint(method = POST, path = "/cluster/configs", permission = CONFIG_EDIT_PERM)
+public class CreateConfigSetAPI extends ConfigSetAPIBase {
+
+  public CreateConfigSetAPI(CoreContainer coreContainer) {
+    super(coreContainer);
+  }
+
+  @Command(name = "create")
+  public void create(PayloadObj<CreateConfigPayload> obj) throws Exception {
+    final CreateConfigPayload createConfigPayload = obj.get();
+    if (configSetService.checkConfigExists(createConfigPayload.name)) {
+      throw new SolrException(
+          SolrException.ErrorCode.BAD_REQUEST,
+          "ConfigSet already exists: " + createConfigPayload.name);
+    }
+
+    // is there a base config that already exists
+    if (!configSetService.checkConfigExists(createConfigPayload.baseConfigSet)) {
+      throw new SolrException(
+          SolrException.ErrorCode.BAD_REQUEST,
+          "Base ConfigSet does not exist: " + createConfigPayload.baseConfigSet);
+    }
+
+    if (!DISABLE_CREATE_AUTH_CHECKS
+        && !isTrusted(obj.getRequest().getUserPrincipal(), coreContainer.getAuthenticationPlugin())
+        && isCurrentlyTrusted(createConfigPayload.baseConfigSet)) {
+      throw new SolrException(
+          SolrException.ErrorCode.UNAUTHORIZED,
+          "Can't create a configset with an unauthenticated request from a trusted "
+              + ConfigSetCmds.BASE_CONFIGSET);
+    }
+
+    final Map<String, Object> configsetCommandMsg = new HashMap<>();
+    configsetCommandMsg.put(NAME, createConfigPayload.name);
+    configsetCommandMsg.put(ConfigSetCmds.BASE_CONFIGSET, createConfigPayload.baseConfigSet);
+    if (!MapUtils.isEmpty(createConfigPayload.properties)) {
+      for (Map.Entry<String, Object> e : createConfigPayload.properties.entrySet()) {
+        configsetCommandMsg.put(
+            ConfigSetCmds.CONFIG_SET_PROPERTY_PREFIX + e.getKey(), e.getValue());
+      }
+    }
+
+    runConfigSetCommand(
+        obj.getResponse(), ConfigSetParams.ConfigSetAction.CREATE, configsetCommandMsg);
+  }
+}
diff --git a/solr/core/src/java/org/apache/solr/handler/configsets/DeleteConfigSetAPI.java b/solr/core/src/java/org/apache/solr/handler/configsets/DeleteConfigSetAPI.java
new file mode 100644
index 00000000000..8c93871e6af
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/handler/configsets/DeleteConfigSetAPI.java
@@ -0,0 +1,62 @@
+/*
+ * 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.configsets;
+
+import static org.apache.solr.client.solrj.SolrRequest.METHOD.DELETE;
+import static org.apache.solr.common.params.CommonParams.NAME;
+import static org.apache.solr.security.PermissionNameProvider.Name.CONFIG_EDIT_PERM;
+
+import com.google.common.collect.Maps;
+import java.util.Map;
+import org.apache.solr.api.EndPoint;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.StringUtils;
+import org.apache.solr.common.params.ConfigSetParams;
+import org.apache.solr.core.CoreContainer;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.response.SolrQueryResponse;
+
+/**
+ * V2 API for deleting an existing configset
+ *
+ * <p>This API (DELETE /v2/cluster/configs/configsetName) is analogous to the v1
+ * /admin/configs?action=DELETE command.
+ */
+public class DeleteConfigSetAPI extends ConfigSetAPIBase {
+
+  public static final String CONFIGSET_NAME_PLACEHOLDER = "name";
+
+  public DeleteConfigSetAPI(CoreContainer coreContainer) {
+    super(coreContainer);
+  }
+
+  @EndPoint(
+      method = DELETE,
+      path = "/cluster/configs/{" + CONFIGSET_NAME_PLACEHOLDER + "}",
+      permission = CONFIG_EDIT_PERM)
+  public void deleteConfigSet(SolrQueryRequest req, SolrQueryResponse rsp) throws Exception {
+    final String configSetName = req.getPathTemplateValues().get("name");
+    if (StringUtils.isEmpty(configSetName)) {
+      throw new SolrException(
+          SolrException.ErrorCode.BAD_REQUEST, "No configset name provided to delete");
+    }
+    final Map<String, Object> configsetCommandMsg = Maps.newHashMap();
+    configsetCommandMsg.put(NAME, configSetName);
+
+    runConfigSetCommand(rsp, ConfigSetParams.ConfigSetAction.DELETE, configsetCommandMsg);
+  }
+}
diff --git a/solr/core/src/java/org/apache/solr/handler/configsets/ListConfigSetsAPI.java b/solr/core/src/java/org/apache/solr/handler/configsets/ListConfigSetsAPI.java
new file mode 100644
index 00000000000..ae872de68af
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/handler/configsets/ListConfigSetsAPI.java
@@ -0,0 +1,49 @@
+/*
+ * 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.configsets;
+
+import static org.apache.solr.client.solrj.SolrRequest.METHOD.GET;
+import static org.apache.solr.security.PermissionNameProvider.Name.CONFIG_READ_PERM;
+
+import java.util.List;
+import org.apache.solr.api.EndPoint;
+import org.apache.solr.client.solrj.SolrResponse;
+import org.apache.solr.cloud.OverseerSolrResponse;
+import org.apache.solr.common.util.NamedList;
+import org.apache.solr.core.CoreContainer;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.response.SolrQueryResponse;
+
+/**
+ * V2 API for adding or updating a single file within a configset.
+ *
+ * <p>This API (GET /v2/cluster/configs) is analogous to the v1 /admin/configs?action=LIST command.
+ */
+public class ListConfigSetsAPI extends ConfigSetAPIBase {
+  public ListConfigSetsAPI(CoreContainer coreContainer) {
+    super(coreContainer);
+  }
+
+  @EndPoint(method = GET, path = "/cluster/configs", permission = CONFIG_READ_PERM)
+  public void listConfigSet(SolrQueryRequest req, SolrQueryResponse rsp) throws Exception {
+    final NamedList<Object> results = new NamedList<>();
+    List<String> configSetsList = configSetService.listConfigs();
+    results.add("configSets", configSetsList);
+    SolrResponse response = new OverseerSolrResponse(results);
+    rsp.getValues().addAll(response.getResponse());
+  }
+}
diff --git a/solr/core/src/java/org/apache/solr/handler/configsets/UploadConfigSetAPI.java b/solr/core/src/java/org/apache/solr/handler/configsets/UploadConfigSetAPI.java
new file mode 100644
index 00000000000..4cb08542386
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/handler/configsets/UploadConfigSetAPI.java
@@ -0,0 +1,130 @@
+/*
+ * 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.configsets;
+
+import static org.apache.solr.client.solrj.SolrRequest.METHOD.PUT;
+import static org.apache.solr.security.PermissionNameProvider.Name.CONFIG_EDIT_PERM;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.invoke.MethodHandles;
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+import org.apache.commons.io.IOUtils;
+import org.apache.solr.api.EndPoint;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.params.ConfigSetParams;
+import org.apache.solr.core.ConfigSetService;
+import org.apache.solr.core.CoreContainer;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.response.SolrQueryResponse;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * V2 API for uploading a new configset (or overwriting an existing one).
+ *
+ * <p>This API (PUT /v2/cluster/configs/configsetName) is analogous to the v1
+ * /admin/configs?action=UPLOAD command.
+ */
+public class UploadConfigSetAPI extends ConfigSetAPIBase {
+
+  public static final String CONFIGSET_NAME_PLACEHOLDER = "name";
+
+  private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+  public UploadConfigSetAPI(CoreContainer coreContainer) {
+    super(coreContainer);
+  }
+
+  @EndPoint(method = PUT, path = "/cluster/configs/{name}", permission = CONFIG_EDIT_PERM)
+  public void uploadConfigSet(SolrQueryRequest req, SolrQueryResponse rsp) throws Exception {
+    ensureConfigSetUploadEnabled();
+
+    final String configSetName = req.getPathTemplateValues().get("name");
+    boolean overwritesExisting = configSetService.checkConfigExists(configSetName);
+    boolean requestIsTrusted =
+        isTrusted(req.getUserPrincipal(), coreContainer.getAuthenticationPlugin());
+    // Get upload parameters
+    boolean allowOverwrite = req.getParams().getBool(ConfigSetParams.OVERWRITE, true);
+    boolean cleanup = req.getParams().getBool(ConfigSetParams.CLEANUP, false);
+    final InputStream inputStream = ensureNonEmptyInputStream(req);
+
+    if (overwritesExisting && !allowOverwrite) {
+      throw new SolrException(
+          SolrException.ErrorCode.BAD_REQUEST,
+          "The configuration " + configSetName + " already exists");
+    }
+
+    List<String> filesToDelete;
+    if (overwritesExisting && cleanup) {
+      filesToDelete = configSetService.getAllConfigFiles(configSetName);
+    } else {
+      filesToDelete = Collections.emptyList();
+    }
+
+    // Create a node for the configuration in zookeeper
+    // For creating the baseZnode, the cleanup parameter is only allowed to be true when
+    // singleFilePath is not passed.
+    createBaseNode(configSetService, overwritesExisting, requestIsTrusted, configSetName);
+
+    try (ZipInputStream zis = new ZipInputStream(inputStream, StandardCharsets.UTF_8)) {
+      boolean hasEntry = false;
+      ZipEntry zipEntry;
+      while ((zipEntry = zis.getNextEntry()) != null) {
+        hasEntry = true;
+        String filePath = zipEntry.getName();
+        filesToDelete.remove(filePath);
+        if (!zipEntry.isDirectory()) {
+          configSetService.uploadFileToConfig(
+              configSetName, filePath, IOUtils.toByteArray(zis), true);
+        }
+      }
+      if (!hasEntry) {
+        throw new SolrException(
+            SolrException.ErrorCode.BAD_REQUEST,
+            "Either empty zipped data, or non-zipped data was uploaded. In order to upload a configSet, you must zip a non-empty directory to upload.");
+      }
+    }
+    deleteUnusedFiles(configSetService, configSetName, filesToDelete);
+
+    // If the request is doing a full trusted overwrite of an untrusted configSet (overwrite=true,
+    // cleanup=true), then trust the configSet.
+    if (cleanup && requestIsTrusted && overwritesExisting && !isCurrentlyTrusted(configSetName)) {
+      Map<String, Object> metadata = Collections.singletonMap("trusted", true);
+      configSetService.setConfigMetadata(configSetName, metadata);
+    }
+  }
+
+  private void deleteUnusedFiles(
+      ConfigSetService configSetService, String configName, List<String> filesToDelete)
+      throws IOException {
+    if (!filesToDelete.isEmpty()) {
+      if (log.isInfoEnabled()) {
+        log.info("Cleaning up {} unused files", filesToDelete.size());
+      }
+      if (log.isDebugEnabled()) {
+        log.debug("Cleaning up unused files: {}", filesToDelete);
+      }
+      configSetService.deleteFilesFromConfig(configName, filesToDelete);
+    }
+  }
+}
diff --git a/solr/core/src/java/org/apache/solr/handler/configsets/UploadConfigSetFileAPI.java b/solr/core/src/java/org/apache/solr/handler/configsets/UploadConfigSetFileAPI.java
new file mode 100644
index 00000000000..d44f3c5ff31
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/handler/configsets/UploadConfigSetFileAPI.java
@@ -0,0 +1,88 @@
+/*
+ * 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.configsets;
+
+import static org.apache.solr.client.solrj.SolrRequest.METHOD.PUT;
+import static org.apache.solr.security.PermissionNameProvider.Name.CONFIG_EDIT_PERM;
+
+import java.io.InputStream;
+import org.apache.commons.io.IOUtils;
+import org.apache.solr.api.EndPoint;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.params.ConfigSetParams;
+import org.apache.solr.core.CoreContainer;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.response.SolrQueryResponse;
+
+/**
+ * V2 API for adding or updating a single file within a configset.
+ *
+ * <p>This API (PUT /v2/cluster/configs/configsetName/someFilePath) is analogous to the v1
+ * /admin/configs?action=UPLOAD&amp;filePath=someFilePath command.
+ */
+public class UploadConfigSetFileAPI extends ConfigSetAPIBase {
+
+  public static final String CONFIGSET_NAME_PLACEHOLDER =
+      UploadConfigSetAPI.CONFIGSET_NAME_PLACEHOLDER;
+  public static final String FILEPATH_PLACEHOLDER = "*";
+
+  private static final String API_PATH =
+      "/cluster/configs/{" + CONFIGSET_NAME_PLACEHOLDER + "}/" + FILEPATH_PLACEHOLDER;
+
+  public UploadConfigSetFileAPI(CoreContainer coreContainer) {
+    super(coreContainer);
+  }
+
+  @EndPoint(method = PUT, path = API_PATH, permission = CONFIG_EDIT_PERM)
+  public void updateConfigSetFile(SolrQueryRequest req, SolrQueryResponse rsp) throws Exception {
+    ensureConfigSetUploadEnabled();
+
+    final String configSetName = req.getPathTemplateValues().get("name");
+    boolean overwritesExisting = configSetService.checkConfigExists(configSetName);
+    boolean requestIsTrusted =
+        isTrusted(req.getUserPrincipal(), coreContainer.getAuthenticationPlugin());
+
+    // Get upload parameters
+
+    String singleFilePath = req.getPathTemplateValues().getOrDefault(FILEPATH_PLACEHOLDER, "");
+    boolean allowOverwrite = req.getParams().getBool(ConfigSetParams.OVERWRITE, true);
+    boolean cleanup = req.getParams().getBool(ConfigSetParams.CLEANUP, false);
+    final InputStream inputStream = ensureNonEmptyInputStream(req);
+
+    String fixedSingleFilePath = singleFilePath;
+    if (fixedSingleFilePath.charAt(0) == '/') {
+      fixedSingleFilePath = fixedSingleFilePath.substring(1);
+    }
+    if (fixedSingleFilePath.isEmpty()) {
+      throw new SolrException(
+          SolrException.ErrorCode.BAD_REQUEST,
+          "The file path provided for upload, '" + singleFilePath + "', is not valid.");
+    } else if (cleanup) {
+      // Cleanup is not allowed while using singleFilePath upload
+      throw new SolrException(
+          SolrException.ErrorCode.BAD_REQUEST,
+          "ConfigSet uploads do not allow cleanup=true when file path is used.");
+    } else {
+      // Create a node for the configuration in config
+      // For creating the baseNode, the cleanup parameter is only allowed to be true when
+      // singleFilePath is not passed.
+      createBaseNode(configSetService, overwritesExisting, requestIsTrusted, configSetName);
+      configSetService.uploadFileToConfig(
+          configSetName, fixedSingleFilePath, IOUtils.toByteArray(inputStream), allowOverwrite);
+    }
+  }
+}
diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/request/beans/CreateConfigPayload.java b/solr/core/src/java/org/apache/solr/handler/configsets/package-info.java
similarity index 62%
copy from solr/solrj/src/java/org/apache/solr/client/solrj/request/beans/CreateConfigPayload.java
copy to solr/core/src/java/org/apache/solr/handler/configsets/package-info.java
index 209b12971bd..bba0f21ed49 100644
--- a/solr/solrj/src/java/org/apache/solr/client/solrj/request/beans/CreateConfigPayload.java
+++ b/solr/core/src/java/org/apache/solr/handler/configsets/package-info.java
@@ -6,7 +6,7 @@
  * (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
+ *      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,
@@ -14,16 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.solr.client.solrj.request.beans;
 
-import java.util.Map;
-import org.apache.solr.common.annotation.JsonProperty;
-import org.apache.solr.common.util.ReflectMapWriter;
-
-public class CreateConfigPayload implements ReflectMapWriter {
-  @JsonProperty(required = true)
-  public String name;
-
-  @JsonProperty public String baseConfigSet;
-  @JsonProperty public Map<String, Object> properties;
-}
+/** V2 API classes for performing CRUD operations on configsets. */
+package org.apache.solr.handler.configsets;
diff --git a/solr/core/src/java/org/apache/solr/request/DelegatingSolrQueryRequest.java b/solr/core/src/java/org/apache/solr/request/DelegatingSolrQueryRequest.java
new file mode 100644
index 00000000000..2c9c20d4f06
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/request/DelegatingSolrQueryRequest.java
@@ -0,0 +1,172 @@
+/*
+ * 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.request;
+
+import io.opentracing.Span;
+import io.opentracing.Tracer;
+import java.security.Principal;
+import java.util.List;
+import java.util.Map;
+import org.apache.solr.cloud.CloudDescriptor;
+import org.apache.solr.common.params.SolrParams;
+import org.apache.solr.common.util.CommandOperation;
+import org.apache.solr.common.util.ContentStream;
+import org.apache.solr.core.CoreContainer;
+import org.apache.solr.core.SolrCore;
+import org.apache.solr.schema.IndexSchema;
+import org.apache.solr.search.SolrIndexSearcher;
+import org.apache.solr.servlet.HttpSolrCall;
+import org.apache.solr.util.RTimerTree;
+
+/**
+ * A {@link SolrQueryRequest} implementation that defers to a delegate in all cases.
+ *
+ * <p>Used primarily in cases where developers want to customize one or more SolrQueryRequest
+ * methods while deferring the remainder to an existing instances.
+ */
+public class DelegatingSolrQueryRequest implements SolrQueryRequest {
+  private final SolrQueryRequest delegate;
+
+  public DelegatingSolrQueryRequest(SolrQueryRequest delegate) {
+    this.delegate = delegate;
+  }
+
+  @Override
+  public SolrParams getParams() {
+    return delegate.getParams();
+  }
+
+  @Override
+  public void setParams(SolrParams params) {
+    delegate.setParams(params);
+  }
+
+  @Override
+  public Iterable<ContentStream> getContentStreams() {
+    return delegate.getContentStreams();
+  }
+
+  @Override
+  public SolrParams getOriginalParams() {
+    return delegate.getOriginalParams();
+  }
+
+  @Override
+  public Map<Object, Object> getContext() {
+    return delegate.getContext();
+  }
+
+  @Override
+  public void close() {
+    delegate.close();
+  }
+
+  @Override
+  public long getStartTime() {
+    return delegate.getStartTime();
+  }
+
+  @Override
+  public RTimerTree getRequestTimer() {
+    return delegate.getRequestTimer();
+  }
+
+  @Override
+  public SolrIndexSearcher getSearcher() {
+    return delegate.getSearcher();
+  }
+
+  @Override
+  public SolrCore getCore() {
+    return delegate.getCore();
+  }
+
+  @Override
+  public IndexSchema getSchema() {
+    return delegate.getSchema();
+  }
+
+  @Override
+  public void updateSchemaToLatest() {
+    delegate.updateSchemaToLatest();
+  }
+
+  @Override
+  public String getParamString() {
+    return delegate.getParamString();
+  }
+
+  @Override
+  public Map<String, Object> getJSON() {
+    return delegate.getJSON();
+  }
+
+  @Override
+  public void setJSON(Map<String, Object> json) {
+    delegate.setJSON(json);
+  }
+
+  @Override
+  public Principal getUserPrincipal() {
+    return delegate.getUserPrincipal();
+  }
+
+  @Override
+  public String getPath() {
+    return delegate.getPath();
+  }
+
+  @Override
+  public Map<String, String> getPathTemplateValues() {
+    return delegate.getPathTemplateValues();
+  }
+
+  @Override
+  public List<CommandOperation> getCommands(boolean validateInput) {
+    return delegate.getCommands(validateInput);
+  }
+
+  @Override
+  public String getHttpMethod() {
+    return delegate.getHttpMethod();
+  }
+
+  @Override
+  public HttpSolrCall getHttpSolrCall() {
+    return delegate.getHttpSolrCall();
+  }
+
+  @Override
+  public Tracer getTracer() {
+    return delegate.getTracer();
+  }
+
+  @Override
+  public Span getSpan() {
+    return delegate.getSpan();
+  }
+
+  @Override
+  public CoreContainer getCoreContainer() {
+    return delegate.getCoreContainer();
+  }
+
+  @Override
+  public CloudDescriptor getCloudDescriptor() {
+    return delegate.getCloudDescriptor();
+  }
+}
diff --git a/solr/core/src/test/org/apache/solr/handler/V2ClusterAPIMappingTest.java b/solr/core/src/test/org/apache/solr/handler/V2ClusterAPIMappingTest.java
index 96364f6c9fa..b2e2b976e8b 100644
--- a/solr/core/src/test/org/apache/solr/handler/V2ClusterAPIMappingTest.java
+++ b/solr/core/src/test/org/apache/solr/handler/V2ClusterAPIMappingTest.java
@@ -33,9 +33,7 @@ import java.util.List;
 import java.util.Map;
 import org.apache.solr.api.Api;
 import org.apache.solr.api.ApiBag;
-import org.apache.solr.cloud.ConfigSetCmds;
 import org.apache.solr.common.params.CollectionParams;
-import org.apache.solr.common.params.ConfigSetParams;
 import org.apache.solr.common.params.SolrParams;
 import org.apache.solr.common.util.CommandOperation;
 import org.apache.solr.common.util.ContentStreamBase;
@@ -71,7 +69,6 @@ public class V2ClusterAPIMappingTest {
     final ClusterAPI clusterAPI = new ClusterAPI(mockCollectionsHandler, mockConfigSetHandler);
     apiBag.registerObject(clusterAPI);
     apiBag.registerObject(clusterAPI.commands);
-    apiBag.registerObject(clusterAPI.configSetCommands);
   }
 
   @Test
@@ -113,70 +110,6 @@ public class V2ClusterAPIMappingTest {
     assertEquals("someId", v1Params.get(REQUESTID));
   }
 
-  // TODO This should probably really get its own class.
-  @Test
-  public void testDeleteConfigetAllParams() throws Exception {
-    final SolrParams v1Params =
-        captureConvertedConfigsetV1Params("/cluster/configs/someConfigset", "DELETE", null);
-
-    assertEquals(ConfigSetParams.ConfigSetAction.DELETE.toString(), v1Params.get(ACTION));
-    assertEquals("someConfigset", v1Params.get(NAME));
-  }
-
-  @Test
-  public void testListConfigsetsAllParams() throws Exception {
-    final SolrParams v1Params = captureConvertedConfigsetV1Params("/cluster/configs", "GET", null);
-
-    assertEquals(ConfigSetParams.ConfigSetAction.LIST.toString(), v1Params.get(ACTION));
-  }
-
-  @Test
-  public void testCreateConfigsetAllParams() throws Exception {
-    final SolrParams v1Params =
-        captureConvertedConfigsetV1Params(
-            "/cluster/configs",
-            "POST",
-            "{'create': {"
-                + "'name': 'new_configset_name', "
-                + "'baseConfigSet':'some_existing_configset', "
-                + "'properties': {'prop1': 'val1', 'prop2': 'val2'}}}");
-
-    assertEquals(ConfigSetParams.ConfigSetAction.CREATE.toString(), v1Params.get(ACTION));
-    assertEquals("new_configset_name", v1Params.get(NAME));
-    assertEquals("some_existing_configset", v1Params.get(ConfigSetCmds.BASE_CONFIGSET));
-    assertEquals("val1", v1Params.get("configSetProp.prop1"));
-    assertEquals("val2", v1Params.get("configSetProp.prop2"));
-  }
-
-  @Test
-  public void testUploadConfigsetAllParams() throws Exception {
-    final SolrParams v1Params =
-        captureConvertedConfigsetV1Params("/cluster/configs/someConfigSetName", "PUT", null);
-
-    assertEquals(ConfigSetParams.ConfigSetAction.UPLOAD.toString(), v1Params.get(ACTION));
-    assertEquals("someConfigSetName", v1Params.get(NAME));
-    // Why does ClusterAPI set the values below as defaults?  They disagree with the v1 defaults in
-    // ConfigSetsHandler.handleConfigUploadRequest
-    assertEquals(true, v1Params.getPrimitiveBool(ConfigSetParams.OVERWRITE));
-    assertEquals(false, v1Params.getPrimitiveBool(ConfigSetParams.CLEANUP));
-  }
-
-  @Test
-  public void testAddFileToConfigsetAllParams() throws Exception {
-    final SolrParams v1Params =
-        captureConvertedConfigsetV1Params(
-            "/cluster/configs/someConfigSetName/some/file/path", "PUT", null);
-
-    assertEquals(ConfigSetParams.ConfigSetAction.UPLOAD.toString(), v1Params.get(ACTION));
-    assertEquals("someConfigSetName", v1Params.get(NAME));
-    assertEquals(
-        "/some/file/path",
-        v1Params.get(
-            ConfigSetParams.FILE_PATH)); // Note the leading '/' that makes the path appear absolute
-    assertEquals(true, v1Params.getPrimitiveBool(ConfigSetParams.OVERWRITE));
-    assertEquals(false, v1Params.getPrimitiveBool(ConfigSetParams.CLEANUP));
-  }
-
   @Test
   public void testAddRoleAllParams() throws Exception {
     final SolrParams v1Params =
diff --git a/solr/core/src/test/org/apache/solr/handler/admin/TestConfigsApi.java b/solr/core/src/test/org/apache/solr/handler/admin/TestConfigsApi.java
deleted file mode 100644
index 5c21c3a295e..00000000000
--- a/solr/core/src/test/org/apache/solr/handler/admin/TestConfigsApi.java
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * 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 static org.apache.solr.client.solrj.SolrRequest.METHOD.DELETE;
-import static org.apache.solr.cloud.Overseer.QUEUE_OPERATION;
-import static org.apache.solr.handler.admin.TestCollectionAPIs.compareOutput;
-
-import java.util.Map;
-import org.apache.solr.SolrTestCaseJ4;
-import org.apache.solr.api.ApiBag;
-import org.apache.solr.common.cloud.ZkNodeProps;
-import org.apache.solr.handler.ClusterAPI;
-import org.apache.solr.response.SolrQueryResponse;
-
-public class TestConfigsApi extends SolrTestCaseJ4 {
-
-  public void testCommands() throws Exception {
-
-    try (ConfigSetsHandler handler =
-        new ConfigSetsHandler(null) {
-
-          @Override
-          protected void checkErrors() {}
-
-          @Override
-          protected void sendToOverseer(
-              SolrQueryResponse rsp, ConfigSetOperation operation, Map<String, Object> result) {
-            result.put(QUEUE_OPERATION, operation.action.toLower());
-            rsp.add(ZkNodeProps.class.getName(), new ZkNodeProps(result));
-          }
-        }) {
-      ApiBag apiBag = new ApiBag(false);
-
-      ClusterAPI o = new ClusterAPI(null, handler);
-      apiBag.registerObject(o);
-      apiBag.registerObject(o.configSetCommands);
-      //      for (Api api : handler.getApis()) apiBag.register(api, emptyMap());
-      compareOutput(
-          apiBag, "/cluster/configs/sample", DELETE, null, "{name :sample, operation:delete}");
-    }
-  }
-}
diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/request/beans/CreateConfigPayload.java b/solr/solrj/src/java/org/apache/solr/client/solrj/request/beans/CreateConfigPayload.java
index 209b12971bd..5f7f2e6687d 100644
--- a/solr/solrj/src/java/org/apache/solr/client/solrj/request/beans/CreateConfigPayload.java
+++ b/solr/solrj/src/java/org/apache/solr/client/solrj/request/beans/CreateConfigPayload.java
@@ -21,9 +21,12 @@ import org.apache.solr.common.annotation.JsonProperty;
 import org.apache.solr.common.util.ReflectMapWriter;
 
 public class CreateConfigPayload implements ReflectMapWriter {
+  public static final String DEFAULT_CONFIGSET =
+      "_default"; // TODO Better location for this in SolrJ?
+
   @JsonProperty(required = true)
   public String name;
 
-  @JsonProperty public String baseConfigSet;
+  @JsonProperty public String baseConfigSet = DEFAULT_CONFIGSET;
   @JsonProperty public Map<String, Object> properties;
 }