You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by no...@apache.org on 2019/09/25 10:30:23 UTC

[lucene-solr] branch jira/SOLR-13722 updated: Use annotated framework for V2 APIs

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

noble pushed a commit to branch jira/SOLR-13722
in repository https://gitbox.apache.org/repos/asf/lucene-solr.git


The following commit(s) were added to refs/heads/jira/SOLR-13722 by this push:
     new bf5e8a1  Use annotated framework for V2 APIs
bf5e8a1 is described below

commit bf5e8a1e389870776315d01c6dfe1c8f775c605f
Author: noble <no...@apache.org>
AuthorDate: Wed Sep 25 20:30:00 2019 +1000

    Use annotated framework for V2 APIs
---
 .../src/java/org/apache/solr/api/AnnotatedApi.java | 138 ++++++
 .../src/java/org/apache/solr/api/CallInfo.java     |  34 ++
 .../core/src/java/org/apache/solr/api/Command.java |  34 ++
 .../src/java/org/apache/solr/api/EndPoint.java     |  38 ++
 .../apache/solr/core/ContainerRequestHandlers.java |  75 +--
 .../java/org/apache/solr/core/CoreContainer.java   |   5 +-
 .../src/java/org/apache/solr/core/FsBlobStore.java |  87 ++--
 .../src/java/org/apache/solr/core/PluginBag.java   |   3 +
 .../solr/handler/admin/BaseHandlerApiSupport.java  |  57 ++-
 .../org/apache/solr/handler/admin/ClusterAPI.java  | 510 ++++++++-------------
 .../solr/handler/admin/CollectionHandlerApi.java   |  22 +-
 .../solr/security/PermissionNameProvider.java      |   2 +
 .../test/org/apache/solr/handler/TestPackages.java |  45 +-
 .../client/solrj/request/CollectionApiMapping.java |   3 -
 ...package.json => cluster.packages.Commands.json} |   2 +-
 .../resources/apispec/cluster.packages.GET.json    |  10 +
 16 files changed, 550 insertions(+), 515 deletions(-)

diff --git a/solr/core/src/java/org/apache/solr/api/AnnotatedApi.java b/solr/core/src/java/org/apache/solr/api/AnnotatedApi.java
new file mode 100644
index 0000000..45b3cba
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/api/AnnotatedApi.java
@@ -0,0 +1,138 @@
+/*
+ * 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.api;
+
+
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.util.CommandOperation;
+import org.apache.solr.common.util.Utils;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.response.SolrQueryResponse;
+import org.apache.solr.security.AuthorizationContext;
+import org.apache.solr.security.PermissionNameProvider;
+
+public class AnnotatedApi extends Api implements PermissionNameProvider {
+  private EndPoint endPoint;
+  private Map<String, Cmd> commands = new HashMap<>();
+
+
+  public AnnotatedApi(Object obj) {
+    super(Utils.getSpec(readSpec(obj.getClass())));
+    Class<?> klas = obj.getClass();
+    if (!Modifier.isPublic(klas.getModifiers())) {
+      throw new RuntimeException(obj.getClass().getName() + " is not public");
+    }
+
+    endPoint = klas.getAnnotation(EndPoint.class);
+
+    for (Method m : klas.getDeclaredMethods()) {
+      Command command = m.getAnnotation(Command.class);
+      if (command == null) continue;
+
+      if (commands.containsKey(command.name())) {
+        throw new RuntimeException("Duplicate commands " + command.name());
+      }
+      commands.put(command.name(), new Cmd(command, obj, m));
+    }
+
+  }
+
+  @Override
+  public Name getPermissionName(AuthorizationContext request) {
+    return endPoint.permission();
+  }
+
+  private static String readSpec(Class klas) {
+    EndPoint endPoint = (EndPoint) klas.getAnnotation(EndPoint.class);
+    return endPoint.spec();
+
+  }
+
+  @Override
+  public void call(SolrQueryRequest req, SolrQueryResponse rsp) {
+    if (commands.size() == 1) {
+      Cmd cmd = commands.get("");
+      if (cmd != null) {
+        cmd.invoke(req, rsp, null);
+        return;
+      }
+    }
+
+    List<CommandOperation> cmds = req.getCommands(true);
+    boolean allExists = true;
+    for (CommandOperation cmd : cmds) {
+      if (!commands.containsKey(cmd.name)) {
+        cmd.addError("No such command supported :" + cmd.name);
+        allExists = false;
+      }
+    }
+    if (!allExists) {
+      throw new ApiBag.ExceptionWithErrObject(SolrException.ErrorCode.BAD_REQUEST, "error processing commands",
+          CommandOperation.captureErrors(cmds));
+    }
+
+    for (CommandOperation cmd : cmds) {
+      commands.get(cmd.name).invoke(req, rsp, cmd);
+    }
+
+    List<Map> errs = CommandOperation.captureErrors(cmds);
+    if (!errs.isEmpty()) {
+      throw new ApiBag.ExceptionWithErrObject(SolrException.ErrorCode.BAD_REQUEST, "Error in executing commands", errs);
+    }
+
+  }
+
+  class Cmd {
+    final Command command;
+    final Method method;
+    final Object obj;
+
+    Cmd(Command command, Object obj, Method method) {
+      if (Modifier.isPublic(method.getModifiers())) {
+        this.command = command;
+        this.obj = obj;
+        this.method = method;
+        Class<?>[] parameterTypes = method.getParameterTypes();
+        if (parameterTypes.length > 1 || parameterTypes[0] != CallInfo.class) {
+          throw new RuntimeException("Invalid params for method " + method);
+        }
+      } else {
+        throw new RuntimeException(method.toString() + " is not a public static method");
+      }
+
+    }
+
+    void invoke(SolrQueryRequest req, SolrQueryResponse rsp, CommandOperation cmd) {
+      try {
+        method.invoke(obj, new CallInfo(req, rsp, cmd));
+      } catch (SolrException se) {
+        throw se;
+      } catch (Exception e) {
+        throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, e);
+      }
+
+    }
+  }
+
+}
diff --git a/solr/core/src/java/org/apache/solr/api/CallInfo.java b/solr/core/src/java/org/apache/solr/api/CallInfo.java
new file mode 100644
index 0000000..f5ae198
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/api/CallInfo.java
@@ -0,0 +1,34 @@
+/*
+ * 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.api;
+
+import org.apache.solr.common.util.CommandOperation;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.response.SolrQueryResponse;
+
+public class CallInfo {
+  public final SolrQueryRequest req;
+  public final SolrQueryResponse rsp;
+  public final CommandOperation command;
+
+  public CallInfo(SolrQueryRequest req, SolrQueryResponse rsp,  CommandOperation command) {
+    this.req = req;
+    this.rsp = rsp;
+    this.command = command;
+  }
+}
diff --git a/solr/core/src/java/org/apache/solr/api/Command.java b/solr/core/src/java/org/apache/solr/api/Command.java
new file mode 100644
index 0000000..45303d6
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/api/Command.java
@@ -0,0 +1,34 @@
+/*
+ * 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.api;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+public @interface Command {
+  /**if this is not a json command , leave it empty.
+   * Keep in mind that you cannot have duplicates.
+   * Only one method per name
+   *
+   */
+  String name() default "";
+}
diff --git a/solr/core/src/java/org/apache/solr/api/EndPoint.java b/solr/core/src/java/org/apache/solr/api/EndPoint.java
new file mode 100644
index 0000000..12f4897
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/api/EndPoint.java
@@ -0,0 +1,38 @@
+/*
+ * 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.api;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.apache.solr.security.PermissionNameProvider;
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.TYPE})
+public @interface EndPoint {
+  /**name of the API spec file without the '.json' suffix
+   */
+  String spec();
+
+
+  /**The well known persmission name if any
+   */
+  PermissionNameProvider.Name permission();
+}
diff --git a/solr/core/src/java/org/apache/solr/core/ContainerRequestHandlers.java b/solr/core/src/java/org/apache/solr/core/ContainerRequestHandlers.java
index a1321e5..86ec41d 100644
--- a/solr/core/src/java/org/apache/solr/core/ContainerRequestHandlers.java
+++ b/solr/core/src/java/org/apache/solr/core/ContainerRequestHandlers.java
@@ -20,25 +20,24 @@ package org.apache.solr.core;
 import java.io.IOException;
 import java.lang.invoke.MethodHandles;
 import java.util.ArrayList;
-import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
-import org.apache.solr.api.Api;
+import org.apache.solr.api.CallInfo;
+import org.apache.solr.api.Command;
+import org.apache.solr.api.EndPoint;
 import org.apache.solr.api.V2HttpCall;
 import org.apache.solr.common.MapWriter;
 import org.apache.solr.common.SolrException;
 import org.apache.solr.common.cloud.ZkStateReader;
 import org.apache.solr.common.util.StrUtils;
 import org.apache.solr.common.util.Utils;
-import org.apache.solr.handler.RequestHandlerBase;
 import org.apache.solr.request.SolrQueryRequest;
 import org.apache.solr.request.SolrRequestHandler;
 import org.apache.solr.response.SolrQueryResponse;
 import org.apache.solr.schema.FieldType;
-import org.apache.solr.security.AuthorizationContext;
 import org.apache.solr.security.PermissionNameProvider;
 import org.apache.solr.util.plugin.PluginInfoInitialized;
 import org.slf4j.Logger;
@@ -49,7 +48,8 @@ import static org.apache.solr.common.params.CommonParams.PACKAGE;
 import static org.apache.solr.common.params.CommonParams.VERSION;
 import static org.apache.solr.core.PluginBag.closeQuietly;
 
-class ContainerRequestHandlers extends RequestHandlerBase implements PermissionNameProvider {
+@EndPoint(spec = "node.ext", permission = PermissionNameProvider.Name.CUSTOM_PERM)
+public class ContainerRequestHandlers {
   private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
 
   final CoreContainer coreContainer;
@@ -60,7 +60,6 @@ class ContainerRequestHandlers extends RequestHandlerBase implements PermissionN
     this.coreContainer = coreContainer;
   }
 
-  @Override
   public void handleRequestBody(SolrQueryRequest req, SolrQueryResponse rsp) {
     int v = req.getParams().getInt(ConfigOverlay.ZNODEVER, -1);
     if (v >= 0) {
@@ -79,27 +78,24 @@ class ContainerRequestHandlers extends RequestHandlerBase implements PermissionN
 
   }
 
-  @Override
-  public Collection<Api> getApis() {
-    return Collections.singleton(new Api(Utils.getSpec("node.ext")) {
-      @Override
-      public void call(SolrQueryRequest req, SolrQueryResponse rsp) {
-        String name = ((V2HttpCall) req.getHttpSolrCall()).getUrlParts().get("handlerName");
-        if (name == null) {
-          handleRequestBody(req, rsp);
-          return;
-        }
-        Handler wrapper = customHandlers.get(name);
-        if (wrapper == null) {
-          String err = StrUtils.formatString(" No such handler: {0}, available handlers : {1}", name, customHandlers.keySet());
-          log.error(err);
-          throw new SolrException(SolrException.ErrorCode.NOT_FOUND, err);
-        }
-        wrapper.handler.handleRequest(req, rsp);
-      }
-    });
+  @Command
+  public void call(CallInfo info) {
+    String name = ((V2HttpCall) info.req.getHttpSolrCall()).getUrlParts().get("handlerName");
+    if (name == null) {
+      handleRequestBody(info.req, info.rsp);
+      return;
+    }
+    Handler wrapper = customHandlers.get(name);
+    if (wrapper == null) {
+      String err = StrUtils.formatString(" No such handler: {0}, available handlers : {1}", name, customHandlers.keySet());
+      log.error(err);
+      throw new SolrException(SolrException.ErrorCode.NOT_FOUND, err);
+    }
+    wrapper.handler.handleRequest(info.req, info.rsp);
+
   }
 
+
   void updateReqHandlers(Map<String, Object> properties) {
     Map m = (Map) properties.getOrDefault(SolrRequestHandler.TYPE, Collections.emptyMap());
     if (m.isEmpty() && customHandlers.isEmpty()) return;
@@ -109,18 +105,18 @@ class ContainerRequestHandlers extends RequestHandlerBase implements PermissionN
     List<Handler> toBeClosed = new ArrayList<>();
     for (Object o : m.entrySet()) {
       Object v = ((Map.Entry) o).getValue();
-      String  name = (String) ((Map.Entry) o).getKey();
+      String name = (String) ((Map.Entry) o).getKey();
       if (v instanceof Map) {
         Map metaData = (Map) v;
         Handler existing = customHandlers.get(name);
         if (existing == null || !existing.meta.equals(metaData)) {
           String klas = (String) metaData.get(FieldType.CLASS_NAME);
           if (klas != null) {
-            newCustomHandlers.put(name,  new Handler(metaData));
+            newCustomHandlers.put(name, new Handler(metaData));
           } else {
             log.error("Invalid requestHandler {}", Utils.toJSONString(v));
           }
-          if(existing != null){
+          if (existing != null) {
             toBeClosed.add(existing);
           }
 
@@ -156,27 +152,6 @@ class ContainerRequestHandlers extends RequestHandlerBase implements PermissionN
     return inst;
   }
 
-  @Override
-  public String getDescription() {
-    return "Custom Handlers";
-  }
-
-
-  @Override
-  public Boolean registerV1() {
-    return Boolean.FALSE;
-  }
-
-  @Override
-  public Boolean registerV2() {
-    return Boolean.TRUE;
-  }
-
-  @Override
-  public Name getPermissionName(AuthorizationContext request) {
-    if (request.getResource().endsWith("/node/ext")) return Name.COLL_READ_PERM;
-    return Name.CUSTOM_PERM;
-  }
 
   class Handler implements MapWriter, PackageListeners.Listener, AutoCloseable {
     SolrRequestHandler handler;
@@ -217,7 +192,7 @@ class ContainerRequestHandlers extends RequestHandlerBase implements PermissionN
 
     @Override
     public void changed(PackageBag.PackageInfo info) {
-      if(this.packageInfo.znodeVersion < info.znodeVersion){
+      if (this.packageInfo.znodeVersion < info.znodeVersion) {
         this.handler = createHandler(meta.attributes);
         this.packageInfo = info;
       }
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 0fd6bfb..db453df 100644
--- a/solr/core/src/java/org/apache/solr/core/CoreContainer.java
+++ b/solr/core/src/java/org/apache/solr/core/CoreContainer.java
@@ -47,6 +47,7 @@ import org.apache.lucene.index.CorruptIndexException;
 import org.apache.lucene.index.IndexWriter;
 import org.apache.lucene.search.IndexSearcher;
 import org.apache.lucene.store.Directory;
+import org.apache.solr.api.AnnotatedApi;
 import org.apache.solr.client.solrj.SolrClient;
 import org.apache.solr.client.solrj.cloud.SolrCloudManager;
 import org.apache.solr.client.solrj.embedded.EmbeddedSolrServer;
@@ -645,8 +646,8 @@ public class CoreContainer {
     reloadSecurityProperties();
     this.backupRepoFactory = new BackupRepositoryFactory(cfg.getBackupRepositoryPlugins());
 
-    containerHandlers.put("/ext", containerRequestHandlers);
-    containerHandlers.put("/blob-get", blobStore.blobRead);
+    containerHandlers.put(new AnnotatedApi(containerRequestHandlers));
+    containerHandlers.put(new AnnotatedApi(blobStore.blobRead));
     createHandler(ZK_PATH, ZookeeperInfoHandler.class.getName(), ZookeeperInfoHandler.class);
     createHandler(ZK_STATUS_PATH, ZookeeperStatusHandler.class.getName(), ZookeeperStatusHandler.class);
     collectionsHandler = createHandler(COLLECTIONS_HANDLER_PATH, cfg.getCollectionsHandlerClass(), CollectionsHandler.class);
diff --git a/solr/core/src/java/org/apache/solr/core/FsBlobStore.java b/solr/core/src/java/org/apache/solr/core/FsBlobStore.java
index e3e1074..6796a98 100644
--- a/solr/core/src/java/org/apache/solr/core/FsBlobStore.java
+++ b/solr/core/src/java/org/apache/solr/core/FsBlobStore.java
@@ -28,7 +28,6 @@ import java.nio.ByteBuffer;
 import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Collection;
 import java.util.Collections;
 import java.util.Date;
 import java.util.List;
@@ -40,7 +39,9 @@ import java.util.function.Consumer;
 
 import org.apache.http.client.HttpClient;
 import org.apache.lucene.util.IOUtils;
-import org.apache.solr.api.Api;
+import org.apache.solr.api.CallInfo;
+import org.apache.solr.api.Command;
+import org.apache.solr.api.EndPoint;
 import org.apache.solr.api.V2HttpCall;
 import org.apache.solr.common.MapWriter;
 import org.apache.solr.common.SolrException;
@@ -49,10 +50,8 @@ import org.apache.solr.common.params.CommonParams;
 import org.apache.solr.common.params.ModifiableSolrParams;
 import org.apache.solr.common.params.SolrParams;
 import org.apache.solr.common.util.Utils;
-import org.apache.solr.handler.RequestHandlerBase;
 import org.apache.solr.request.SolrQueryRequest;
 import org.apache.solr.response.SolrQueryResponse;
-import org.apache.solr.security.AuthorizationContext;
 import org.apache.solr.security.PermissionNameProvider;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -284,64 +283,38 @@ public class FsBlobStore {
   }
 
 
-  class BlobRead extends RequestHandlerBase implements PermissionNameProvider {
-
-    @Override
-    public void handleRequestBody(SolrQueryRequest req, SolrQueryResponse rsp) {
-
-    }
-
-    @Override
-    public String getDescription() {
-      return "List fetch blobs";
-    }
-
-    @Override
-    public Name getPermissionName(AuthorizationContext request) {
-      return Name.BLOB_READ;
-    }
+  @EndPoint(spec = "node.blob.GET",
+  permission = PermissionNameProvider.Name.BLOB_READ)
+  public class BlobRead {
+
+    @Command
+    public void get(CallInfo info){
+      SolrQueryRequest req = info.req;
+      SolrQueryResponse rsp = info.rsp;
+      String sha256 = ((V2HttpCall) req.getHttpSolrCall()).getUrlParts().get("sha256");
+      if (sha256 == null) {
+        rsp.add("blob", fileList(req.getParams()));
+      } else {
+        if (!fetchBlobToFS(sha256)) {
+          throw new SolrException(SolrException.ErrorCode.NOT_FOUND, "No such blob");
+        }
 
-    @Override
-    public Collection<Api> getApis() {
-      return Collections.singleton(new Api(Utils.getSpec("node.blob.GET")) {
-        @Override
-        public void call(SolrQueryRequest req, SolrQueryResponse rsp) {
-          String sha256 = ((V2HttpCall) req.getHttpSolrCall()).getUrlParts().get("sha256");
-          if (sha256 == null) {
-            rsp.add("blob", fileList(req.getParams()));
+        ModifiableSolrParams solrParams = new ModifiableSolrParams();
+        solrParams.add(CommonParams.WT, FILE_STREAM);
+        req.setParams(SolrParams.wrapDefaults(solrParams, req.getParams()));
+        rsp.add(FILE_STREAM, (SolrCore.RawWriter) os -> {
+          ByteBuffer b = tmpBlobs.get(sha256);
+          if (b != null) {
+            os.write(b.array(), b.arrayOffset(), b.limit());
           } else {
-            if (!fetchBlobToFS(sha256)) {
-              throw new SolrException(SolrException.ErrorCode.NOT_FOUND, "No such blob");
+            File file = new File(getBlobsPath().toFile(), sha256);
+            try (FileInputStream is = new FileInputStream(file)) {
+              org.apache.commons.io.IOUtils.copy(is, os);
             }
-
-            ModifiableSolrParams solrParams = new ModifiableSolrParams();
-            solrParams.add(CommonParams.WT, FILE_STREAM);
-            req.setParams(SolrParams.wrapDefaults(solrParams, req.getParams()));
-            rsp.add(FILE_STREAM, (SolrCore.RawWriter) os -> {
-              ByteBuffer b = tmpBlobs.get(sha256);
-              if (b != null) {
-                os.write(b.array(), b.arrayOffset(), b.limit());
-              } else {
-                File file = new File(getBlobsPath().toFile(), sha256);
-                try (FileInputStream is = new FileInputStream(file)) {
-                  org.apache.commons.io.IOUtils.copy(is, os);
-                }
-              }
-            });
           }
-        }
-      });
-    }
-
-
-    @Override
-    public Boolean registerV1() {
-      return Boolean.FALSE;
+        });
+      }
     }
 
-    @Override
-    public Boolean registerV2() {
-      return Boolean.TRUE;
-    }
   }
 }
diff --git a/solr/core/src/java/org/apache/solr/core/PluginBag.java b/solr/core/src/java/org/apache/solr/core/PluginBag.java
index 3c2c37b..433aad5 100644
--- a/solr/core/src/java/org/apache/solr/core/PluginBag.java
+++ b/solr/core/src/java/org/apache/solr/core/PluginBag.java
@@ -199,6 +199,9 @@ public class PluginBag<T> implements AutoCloseable {
     if(old != null)  closeQuietly(old);
     return old == null ? null : old.get();
   }
+  public void put(Api api){
+    apiBag.register(api,Collections.emptyMap());
+  }
 
   public PluginHolder<T> put(String name, PluginHolder<T> plugin) {
     Boolean registerApi = null;
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/BaseHandlerApiSupport.java b/solr/core/src/java/org/apache/solr/handler/admin/BaseHandlerApiSupport.java
index a88c123..07928a9 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/BaseHandlerApiSupport.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/BaseHandlerApiSupport.java
@@ -54,28 +54,34 @@ import static org.apache.solr.common.util.StrUtils.splitSmart;
 public abstract class BaseHandlerApiSupport implements ApiSupport {
   private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
 
-  protected final Map<SolrRequest.METHOD, Map<V2EndPoint, List<ApiCommand>>> commandsMapping;
-
-  protected BaseHandlerApiSupport() {
-    commandsMapping = new HashMap<>();
-    for (ApiCommand cmd : getCommands()) {
-      Map<V2EndPoint, List<ApiCommand>> m = commandsMapping.get(cmd.meta().getHttpMethod());
-      if (m == null) commandsMapping.put(cmd.meta().getHttpMethod(), m = new HashMap<>());
-      List<ApiCommand> list = m.get(cmd.meta().getEndPoint());
-      if (list == null) m.put(cmd.meta().getEndPoint(), list = new ArrayList<>());
-      list.add(cmd);
-    }
-  }
+  List<Api> apis;
 
   @Override
   public synchronized Collection<Api> getApis() {
-    ImmutableList.Builder<Api> l = ImmutableList.builder();
-    for (V2EndPoint op : getEndPoints()) l.add(getApi(op));
-    return l.build();
+    if (apis == null) {
+      Map<SolrRequest.METHOD, Map<V2EndPoint, List<ApiCommand>>> commandsMapping = new HashMap<>();
+      for (ApiCommand cmd : getCommands()) {
+        Map<V2EndPoint, List<ApiCommand>> m = commandsMapping.get(cmd.meta().getHttpMethod());
+        if (m == null) commandsMapping.put(cmd.meta().getHttpMethod(), m = new HashMap<>());
+        List<ApiCommand> list = m.get(cmd.meta().getEndPoint());
+        if (list == null) m.put(cmd.meta().getEndPoint(), list = new ArrayList<>());
+        list.add(cmd);
+      }
+      ImmutableList.Builder<Api> l = ImmutableList.builder();
+      for (V2EndPoint op : getEndPoints()) l.add(getApi(commandsMapping, op));
+      l.addAll(getV2OnlyApis());
+      apis = l.build();
+    }
+    return apis;
+
+  }
+
+  protected Collection<Api> getV2OnlyApis() {
+    return Collections.EMPTY_LIST;
   }
 
 
-  private Api getApi(final V2EndPoint op) {
+  private Api getApi(Map<SolrRequest.METHOD, Map<V2EndPoint, List<ApiCommand>>> commandsMapping, final V2EndPoint op) {
     final BaseHandlerApiSupport apiHandler = this;
     return new Api(Utils.getSpec(op.getSpecName())) {
       @Override
@@ -84,8 +90,8 @@ public abstract class BaseHandlerApiSupport implements ApiSupport {
         SolrRequest.METHOD method = SolrRequest.METHOD.valueOf(req.getHttpMethod());
         List<ApiCommand> commands = commandsMapping.get(method).get(op);
         try {
-          if(commands!=null &&  commands.size() == 1 && commands.get(0).isRaw()){
-            commands.get(0).invoke(req,rsp, apiHandler);
+          if (commands != null && commands.size() == 1 && commands.get(0).isRaw()) {
+            commands.get(0).invoke(req, rsp, apiHandler);
             return;
           }
           if (method == POST) {
@@ -191,7 +197,7 @@ public abstract class BaseHandlerApiSupport implements ApiSupport {
 
           @Override
           public Map toMap(Map<String, Object> suppliedMap) {
-            for(Iterator<String> it=getParameterNamesIterator(); it.hasNext(); ) {
+            for (Iterator<String> it = getParameterNamesIterator(); it.hasNext(); ) {
               final String param = it.next();
               String key = cmd.meta().getParamSubstitute(param);
               Object o = key.indexOf('.') > 0 ?
@@ -206,10 +212,10 @@ public abstract class BaseHandlerApiSupport implements ApiSupport {
                   Number.class.isAssignableFrom(oClass) ||
                   Character.class.isAssignableFrom(oClass) ||
                   Boolean.class.isAssignableFrom(oClass)) {
-                suppliedMap.put(param,String.valueOf(o));
-              } else if (List.class.isAssignableFrom(oClass) && ((List)o).get(0) instanceof String ) {
+                suppliedMap.put(param, String.valueOf(o));
+              } else if (List.class.isAssignableFrom(oClass) && ((List) o).get(0) instanceof String) {
                 List<String> l = (List<String>) o;
-                suppliedMap.put( param, l.toArray(new String[0]));
+                suppliedMap.put(param, l.toArray(new String[0]));
               } else {
                 // Lists pass through but will require special handling downstream
                 // if they contain non-string elements.
@@ -227,15 +233,16 @@ public abstract class BaseHandlerApiSupport implements ApiSupport {
   protected abstract Collection<V2EndPoint> getEndPoints();
 
 
-  public interface ApiCommand  {
+  public interface ApiCommand {
 
     CommandMeta meta();
 
 
-    /** If true, do not do anything with the payload. The command implementation will do everything
+    /**
+     * If true, do not do anything with the payload. The command implementation will do everything
      */
 
-    default boolean isRaw(){
+    default boolean isRaw() {
       return false;
     }
 
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/ClusterAPI.java b/solr/core/src/java/org/apache/solr/handler/admin/ClusterAPI.java
index c26503e..8826665 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/ClusterAPI.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/ClusterAPI.java
@@ -30,13 +30,14 @@ import java.util.Objects;
 import java.util.stream.Collectors;
 
 import org.apache.http.client.HttpClient;
-import org.apache.solr.api.ApiBag;
-import org.apache.solr.client.solrj.SolrRequest;
-import org.apache.solr.client.solrj.request.CollectionApiMapping;
+import org.apache.solr.api.AnnotatedApi;
+import org.apache.solr.api.Api;
+import org.apache.solr.api.CallInfo;
+import org.apache.solr.api.Command;
+import org.apache.solr.api.EndPoint;
 import org.apache.solr.common.MapWriter;
 import org.apache.solr.common.SolrException;
 import org.apache.solr.common.cloud.ClusterProperties;
-import org.apache.solr.common.cloud.SolrZkClient;
 import org.apache.solr.common.cloud.ZkStateReader;
 import org.apache.solr.common.params.CommonParams;
 import org.apache.solr.common.util.CommandOperation;
@@ -46,56 +47,54 @@ import org.apache.solr.common.util.Utils;
 import org.apache.solr.core.CoreContainer;
 import org.apache.solr.core.PackageBag;
 import org.apache.solr.handler.SolrConfigHandler;
-import org.apache.solr.handler.admin.BaseHandlerApiSupport.ApiCommand;
-import org.apache.solr.request.SolrQueryRequest;
 import org.apache.solr.request.SolrRequestHandler;
-import org.apache.solr.response.SolrQueryResponse;
 import org.apache.solr.util.RTimer;
 import org.apache.solr.util.SimplePostTool;
-import org.apache.zookeeper.KeeperException;
 import org.apache.zookeeper.data.Stat;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import static java.util.Arrays.asList;
-import static java.util.Collections.singletonList;
-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.request.CollectionApiMapping.EndPoint.CLUSTER_CMD;
-import static org.apache.solr.client.solrj.request.CollectionApiMapping.EndPoint.CLUSTER_NODES;
-import static org.apache.solr.client.solrj.request.CollectionApiMapping.EndPoint.CLUSTER_PKG;
-import static org.apache.solr.client.solrj.request.CollectionApiMapping.EndPoint.CLUSTER_REPO;
 import static org.apache.solr.common.params.CommonParams.PACKAGES;
-import static org.apache.solr.common.util.CommandOperation.captureErrors;
 import static org.apache.solr.common.util.StrUtils.formatString;
 import static org.apache.solr.core.BlobRepository.sha256Digest;
 import static org.apache.solr.core.ConfigOverlay.ZNODEVER;
 import static org.apache.solr.core.RuntimeLib.SHA256;
+import static org.apache.solr.security.PermissionNameProvider.Name.BLOB_WRITE;
+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.PKG_EDIT;
+import static org.apache.solr.security.PermissionNameProvider.Name.PKG_READ;
 
 //implements  v2 only APIs at /cluster/* end point
 class ClusterAPI {
+  private final CoreContainer coreContainer;
   private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
 
+  ClusterAPI(CoreContainer coreContainer) {
+    this.coreContainer = coreContainer;
+  }
+
+
   //sync the cluster props in every node
-  static void syncClusterProps(ApiInfo info) throws IOException {
-    CoreContainer cc = info.coreContainer;
+  void syncClusterProps(CallInfo info) throws IOException {
     Stat stat = new Stat();
-    Map<String, Object> clusterProperties = new ClusterProperties(cc.getZkController().getZkClient()).getClusterProperties(stat);
+    Map<String, Object> clusterProperties = new ClusterProperties(coreContainer.getZkController().getZkClient()).getClusterProperties(stat);
     try {
-      cc.getPackageBag().onChange(clusterProperties);
+      coreContainer.getPackageBag().onChange(clusterProperties);
     } catch (SolrException e) {
-      log.error("error executing command : " + info.op.jsonStr(), e);
+      log.error("error executing command : " + info.command.jsonStr(), e);
       throw e;
     } catch (Exception e) {
-      log.error("error executing command : " + info.op.jsonStr(), e);
+      log.error("error executing command : " + info.command.jsonStr(), e);
       throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "error executing command : ", e);
     }
     log.info("current version of clusterprops.json is {} , trying to get every node to update ", stat.getVersion());
     log.debug("The current clusterprops.json:  {}", clusterProperties);
-    waitForStateSync(stat.getVersion(), cc);
+    waitForStateSync(stat.getVersion(), coreContainer);
   }
 
-  private static void waitForStateSync(int expectedVersion, CoreContainer coreContainer) {
+  void waitForStateSync(int expectedVersion, CoreContainer coreContainer) {
     final RTimer timer = new RTimer();
     int waitTimeSecs = 30;
     // get a list of active replica cores to query for the schema zk version (skipping this core of course)
@@ -133,347 +132,227 @@ class ClusterAPI {
 
   }
 
+  public List<Api> getAllApis() {
+    List<Api> result = new ArrayList<>();
+    result.add(new AnnotatedApi( new ClusterAPI.ListNodes()));
+    result.add(new AnnotatedApi(new ClusterAPI.BlobWrite()));
+    result.add(new AnnotatedApi(new ClusterAPI.PkgRead()));
+    result.add(new AnnotatedApi(new ClusterAPI.PkgEdit()));
+    result.add(new AnnotatedApi(new ClusterAPI.ClusterCommands()));
+    return result;
+  }
 
-  enum Commands implements ApiCommand {
+  @EndPoint(
+      spec = "cluster.packages.Commands",
+      permission = PKG_EDIT
+  )
+  public class PkgEdit {
 
-    ADD_REPO(CLUSTER_REPO, POST, "add") {
-      @Override
-      void call(ApiInfo info) throws Exception {
-        repositoryCRUD(info);
+    @Command(name = "add")
+    public void add(CallInfo callInfo) throws Exception {
+      if (addUpdatePackage(callInfo)) {
+        syncClusterProps(callInfo);
       }
-    },
-    UPDATE_REPO(CLUSTER_REPO, POST, "update") {
-      @Override
-      void call(ApiInfo info) throws Exception {
-        repositoryCRUD(info);
-      }
-    },
-    DELETE_REPO(CLUSTER_REPO, POST, "delete") {
-      @Override
-      void call(ApiInfo info) throws Exception {
-        repositoryCRUD(info);
 
-      }
-    },
+    }
 
-    GET_NODES(CLUSTER_NODES, GET, null) {
-      @Override
-      void call(ApiInfo info) throws Exception {
-        info.rsp.add("nodes", info.coreContainer.getZkController().getClusterState().getLiveNodes());
+    @Command(name = "update")
+    public void update(CallInfo callInfo) throws Exception {
+      if (addUpdatePackage(callInfo)) {
+        syncClusterProps(callInfo);
       }
+    }
 
-    },
-    POST_BLOB(CollectionApiMapping.EndPoint.CLUSTER_BLOB, POST, null) {
-      @Override
-      void call(ApiInfo info) throws Exception {
-        CoreContainer coreContainer = ((CollectionHandlerApi) info.apiHandler).handler.coreContainer;
-        Iterable<ContentStream> streams = info.req.getContentStreams();
-        if (streams == null) throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "no payload");
-        String sha256 = null;
-        ContentStream stream = streams.iterator().next();
-        try {
-          ByteBuffer buf = SimplePostTool.inputStreamToByteArray(stream.getStream());
-          sha256 = sha256Digest(buf);
-          coreContainer.getBlobStore().distributeBlob(buf, sha256);
-          info.rsp.add(SHA256, sha256);
+    @Command(name = "delete")
+    public void delPkg(CallInfo info) throws Exception {
+      if (deletePackage(info)) {
+        syncClusterProps(info);
+      }
+    }
 
-        } catch (IOException e) {
-          throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, e);
-        }
 
+    boolean deletePackage(CallInfo params) throws Exception {
+      if (checkEnabled(params)) return false;
+      String name = params.command.getStr(CommandOperation.ROOT_OBJ);
+      ClusterProperties clusterProperties = new ClusterProperties(coreContainer.getZkController().getZkClient());
+      Map<String, Object> props = clusterProperties.getClusterProperties();
+      List<String> pathToLib = asList(PACKAGES, name);
+      Map existing = (Map) Utils.getObjectByPath(props, false, pathToLib);
+      if (existing == null) {
+        params.command.addError("No such package : " + name);
+        return false;
       }
+      Map delta = new LinkedHashMap();
+      Utils.setObjectByPath(delta, pathToLib, null, true);
+      clusterProperties.setClusterProperties(delta);
+      return true;
+    }
 
-      @Override
-      public boolean isRaw() {
-        return true;
-      }
-    },
-
-    SET_CLUSTER_PROPERTY_OBJ(CLUSTER_CMD,
-        POST,
-        "set-obj-property") {
-      @Override
-      void call(ApiInfo info) throws Exception {
-        ClusterProperties clusterProperties = new ClusterProperties(info.coreContainer.getZkController().getZkClient());
-        try {
-          clusterProperties.setClusterProperties(info.op.getDataMap());
-        } catch (Exception e) {
-          throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Error in API", e);
-        }
-      }
 
-    },
-    LIST_PKG(CLUSTER_PKG, GET, null){
-      @Override
-      void call(ApiInfo info) throws Exception {
-        ClusterProperties clusterProperties = new ClusterProperties(info.coreContainer.getZkController().getZkClient());
-        info.rsp.add(PACKAGES, clusterProperties.getClusterProperty(PACKAGES, MapWriter.EMPTY));
-      }
-    },
-    ADD_PACKAGE(CLUSTER_PKG,
-        POST,
-        "add") {
-      @Override
-      void call(ApiInfo info) throws Exception {
-        if (addUpdatePackage(info)) syncClusterProps(info);
-      }
-    },
-    UPDATE_PACKAGE(CLUSTER_PKG,
-        POST,
-        "update") {
-      @Override
-      void call(ApiInfo info) throws Exception {
-        if (addUpdatePackage(info)) syncClusterProps(info);
-      }
-    },
-    DELETE_PKG(CLUSTER_PKG,
-        POST,
-        "delete") {
-      @Override
-      void call(ApiInfo info) throws Exception {
-        if (deletePackage(info)) syncClusterProps(info);
+    boolean checkEnabled(CallInfo info) {
+      if (!PackageBag.enablePackage) {
+        info.command.addError("node not started with enable.package=true");
+        return true;
       }
+      return false;
+    }
 
-      boolean deletePackage(ApiInfo params) throws Exception {
-        if (checkEnabled(params)) return false;
-        String name = params.op.getStr(CommandOperation.ROOT_OBJ);
-        ClusterProperties clusterProperties = new ClusterProperties(params.coreContainer.getZkController().getZkClient());
-        Map<String, Object> props = clusterProperties.getClusterProperties();
-        List<String> pathToLib = asList(PACKAGES, name);
-        Map existing = (Map) Utils.getObjectByPath(props, false, pathToLib);
-        if (existing == null) {
-          params.op.addError("No such runtimeLib : " + name);
+    boolean addUpdatePackage(CallInfo params) throws Exception {
+      if (checkEnabled(params)) return false;
+      CommandOperation op = params.command;
+      String name = op.getStr("name");
+      ClusterProperties clusterProperties = new ClusterProperties(coreContainer.getZkController().getZkClient());
+      Map<String, Object> props = clusterProperties.getClusterProperties();
+      List<String> pathToLib = asList(PACKAGES, name);
+      Map existing = (Map) Utils.getObjectByPath(props, false, pathToLib);
+      Map<String, Object> dataMap = Utils.getDeepCopy(op.getDataMap(), 3);
+      PackageBag.PackageInfo packageInfo = new PackageBag.PackageInfo(dataMap, 0);
+
+      if ("add".equals(op.name)) {
+        if (existing != null) {
+          op.addError(StrUtils.formatString("The package with a name ''{0}'' already exists ", name));
           return false;
         }
-        Map delta = new LinkedHashMap();
-        Utils.setObjectByPath(delta, pathToLib, null, true);
-        clusterProperties.setClusterProperties(delta);
-        return true;
-      }
-    },
-    ADD_REQ_HANDLER(CLUSTER_CMD,
-        POST,
-        "add-requesthandler") {
-      @Override
-      void call(ApiInfo info) throws Exception {
-        if (addRequestHandler(info)) syncClusterProps(info);
-      }
-
-      boolean addRequestHandler(ApiInfo info) throws Exception {
-        Map data = info.op.getDataMap();
-        String name = (String) data.get("name");
-        CoreContainer coreContainer = info.coreContainer;
-        ClusterProperties clusterProperties = new ClusterProperties(coreContainer.getZkController().getZkClient());
-        Map<String, Object> map = clusterProperties.getClusterProperties();
-        if (Utils.getObjectByPath(map, false, asList(SolrRequestHandler.TYPE, name)) != null) {
-          info.op.addError("A requestHandler already exists with the said name");
+      } else {// this is an update command
+        if (existing == null) {
+          op.addError(StrUtils.formatString("The package with a name ''{0}'' does not exist", name));
           return false;
         }
-        Map m = new LinkedHashMap();
-        Utils.setObjectByPath(m, asList(SolrRequestHandler.TYPE, name), data, true);
-        clusterProperties.setClusterProperties(m);
-        return true;
-      }
-    },
-    DELETE_REQ_HANDLER(CLUSTER_CMD,
-        POST,
-        "delete-requesthandler") {
-      @Override
-      void call(ApiInfo info) throws Exception {
-        if (deleteReqHandler(info)) syncClusterProps(info);
-      }
-
-      boolean deleteReqHandler(ApiInfo params) throws Exception {
-        String name = params.op.getStr("");
-        ClusterProperties clusterProperties = new ClusterProperties(params.coreContainer.getZkController().getZkClient());
-        Map<String, Object> map = clusterProperties.getClusterProperties();
-        if (Utils.getObjectByPath(map, false, asList(SolrRequestHandler.TYPE, name)) == null) {
-          params.op.addError("NO such requestHandler with name :");
+        PackageBag.PackageInfo oldInfo = new PackageBag.PackageInfo(existing, 1);
+        if (Objects.equals(oldInfo, packageInfo)) {
+          op.addError("Trying to update a package with the same data");
           return false;
         }
-        Map m = new LinkedHashMap();
-        Utils.setObjectByPath(m, asList(SolrRequestHandler.TYPE, name), null, true);
-        clusterProperties.setClusterProperties(m);
-        return true;
+        packageInfo.oldBlob = oldInfo.blobs.stream()
+            .map(it -> it.sha256)
+            .collect(Collectors.toList());
       }
-    }
-
-    ;
-
-    private CollectionApiMapping.CommandMeta meta;
-
-    Commands(CollectionApiMapping.V2EndPoint endPoint, SolrRequest.METHOD method, String cmdName) {
-      meta = new CollectionApiMapping.CommandMeta() {
-        @Override
-        public String getName() {
-          return cmdName;
+      try {
+        List<String> errs = packageInfo.validate(coreContainer);
+        if (!errs.isEmpty()) {
+          for (String err : errs) op.addError(err);
+          return false;
         }
+      } catch (FileNotFoundException fnfe) {
+        op.addError(fnfe.getMessage());
+        return false;
 
-        @Override
-        public SolrRequest.METHOD getHttpMethod() {
-          return method;
-        }
+      } catch (SolrException e) {
+        log.error("Error loading package ", e);
+        op.addError(e.getMessage());
+        return false;
+      }
 
-        @Override
-        public CollectionApiMapping.V2EndPoint getEndPoint() {
-          return endPoint;
-        }
-      };
+      Map delta = new LinkedHashMap();
+      Utils.setObjectByPath(delta, pathToLib, packageInfo, true);
+      clusterProperties.setClusterProperties(delta);
+      return true;
 
     }
 
-    @Override
-    public CollectionApiMapping.CommandMeta meta() {
-      return meta;
+  }
 
+  @EndPoint(
+      spec = "cluster.packages.GET",
+      permission = PKG_READ
+  )
+  public class PkgRead {
+    @Command
+    public void list(CallInfo info) throws IOException {
+      ClusterProperties clusterProperties = new ClusterProperties(coreContainer. getZkController().getZkClient());
+      info.rsp.add(PACKAGES, clusterProperties.getClusterProperty(PACKAGES, MapWriter.EMPTY));
     }
+  }
 
-    @Override
-    public void invoke(SolrQueryRequest req, SolrQueryResponse rsp, BaseHandlerApiSupport apiHandler) throws Exception {
-      CommandOperation op = null;
-      if (meta().getHttpMethod() == SolrRequest.METHOD.POST) {
-        if (meta.getName() != null) {
-          List<CommandOperation> commands = req.getCommands(true);
-          if (commands == null || commands.size() != 1)
-            throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "should have exactly one command");
-          op = commands.get(0);
-        }
-      }
+  @EndPoint(spec = "cluster.blob",
+      permission = BLOB_WRITE)
+  public class BlobWrite {
+    @Command
+    public void add(CallInfo info) {
+      Iterable<ContentStream> streams = info.req.getContentStreams();
+      if (streams == null) throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "no payload");
+      String sha256 = null;
+      ContentStream stream = streams.iterator().next();
+      try {
+        ByteBuffer buf = SimplePostTool.inputStreamToByteArray(stream.getStream());
+        sha256 = sha256Digest(buf);
+        coreContainer.getBlobStore().distributeBlob(buf, sha256);
+        info.rsp.add(SHA256, sha256);
 
-      call(new ApiInfo(req, rsp, apiHandler, op,
-          ((CollectionHandlerApi) apiHandler).handler.coreContainer));
-      if (op != null && op.hasError()) {
-        throw new ApiBag.ExceptionWithErrObject(SolrException.ErrorCode.BAD_REQUEST, "error processing commands", captureErrors(singletonList(op)));
+      } catch (IOException e) {
+        throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, e);
       }
+
     }
 
-    abstract void call(ApiInfo info) throws Exception;
+  }
 
+  @EndPoint(spec = "cluster.nodes",
+      permission = COLL_READ_PERM)
 
+  public class ListNodes {
+    @Command
+    public void list(CallInfo info) {
+      info.rsp.add("nodes", coreContainer.getZkController().getClusterState().getLiveNodes());
+    }
   }
 
-  private static void repositoryCRUD(ApiInfo info) {
-    try {
-      SolrZkClient zkClient = info.coreContainer.getZkController().getZkClient();
-      Map data = Utils.getDeepCopy(Utils.getJson(zkClient, ZkStateReader.PACKAGE_REPO, true),3);
-      Map<String, Object> dataMap = null;
-      String name = null;
-      name = info.op.getCommandData() instanceof String ?
-          (String) info.op.getCommandData() :
-          info.op.getStr("name");
-
-      List<String> path = asList("repository", name);
-      boolean contains = Utils.getObjectByPath(data, false, path) != null;
-
-      if(info.op.name.equals(Commands.ADD_REPO.meta.getName())){
-        if(contains){
-          info.op.addError("repository "+ name + " already exists");
-          return;
-        }
-       dataMap = info.op.getDataMap();
+  @EndPoint(spec = "cluster.Commands",
+      permission = COLL_EDIT_PERM)
+  public class ClusterCommands {
+    @Command(name = "add-requesthandler")
+    public void addHandler(CallInfo info) throws Exception {
+      if (addRequestHandler(info)) syncClusterProps(info);
+    }
 
-      } else if(info.op.name.equals(Commands.UPDATE_REPO.meta.getName())) {
-        if(!contains){
-          info.op.addError("repository "+ name + " does not exist");
-          return;
-        }
-        dataMap = info.op.getDataMap();
+    @Command(name = "delete-requesthandler")
+    public void delHandler(CallInfo info) throws Exception {
+      if (deleteReqHandler(info)) syncClusterProps(info);
+    }
 
-      } else if(info.op.name.equals(Commands.DELETE_REPO.meta.getName())){
-        if(!contains){
-          info.op.addError("repository "+ name + " does not exist");
-          return;
-        }
+    @Command(name = "set-obj-property")
+    public void setObj(CallInfo info) {
+      ClusterProperties clusterProperties = new ClusterProperties(coreContainer.getZkController().getZkClient());
+      try {
+        clusterProperties.setClusterProperties(info.command.getDataMap());
+      } catch (Exception e) {
+        throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Error in API", e);
       }
 
-      Map delta = new LinkedHashMap();
-      Utils.setObjectByPath(delta, path, dataMap, true);
-      updateRepository(zkClient, delta);
-    } catch (SolrException se){
-      throw se;
-    } catch (Exception e) {
-      throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, e);
     }
-  }
 
-  static boolean addUpdatePackage(ApiInfo params) throws Exception {
-    if (checkEnabled(params)) return false;
-    CommandOperation op = params.op;
-    String name = op.getStr("name");
-    ClusterProperties clusterProperties = new ClusterProperties(params.coreContainer.getZkController().getZkClient());
-    Map<String, Object> props = clusterProperties.getClusterProperties();
-    List<String> pathToLib = asList(PACKAGES, name);
-    Map existing = (Map) Utils.getObjectByPath(props, false, pathToLib);
-    Map<String, Object> dataMap = Utils.getDeepCopy(op.getDataMap(), 3);
-    PackageBag.PackageInfo packageInfo = new PackageBag.PackageInfo(dataMap, 0);
-
-    if (ClusterAPI.Commands.ADD_PACKAGE.meta().getName().equals(op.name)) {
-      if (existing != null) {
-        op.addError(StrUtils.formatString("The package with a name ''{0}'' already exists ", name));
-        return false;
-      }
-    } else {// this is an update command
-      if (existing == null) {
-        op.addError(StrUtils.formatString("The package with a name ''{0}'' does not exist", name));
-        return false;
-      }
-      PackageBag.PackageInfo oldInfo = new PackageBag.PackageInfo(existing, 1);
-      if (Objects.equals(oldInfo, packageInfo)) {
-        op.addError("Trying to update a package with the same data");
+
+    boolean addRequestHandler(CallInfo info) throws Exception {
+      Map data = info.command.getDataMap();
+      String name = (String) data.get("name");
+      ClusterProperties clusterProperties = new ClusterProperties(coreContainer.getZkController().getZkClient());
+      Map<String, Object> map = clusterProperties.getClusterProperties();
+      if (Utils.getObjectByPath(map, false, asList(SolrRequestHandler.TYPE, name)) != null) {
+        info.command.addError("A requestHandler already exists with the said name");
         return false;
       }
-      packageInfo.oldBlob = oldInfo.blobs.stream()
-          .map(it -> it.sha256)
-          .collect(Collectors.toList());
+      Map m = new LinkedHashMap();
+      Utils.setObjectByPath(m, asList(SolrRequestHandler.TYPE, name), data, true);
+      clusterProperties.setClusterProperties(m);
+      return true;
     }
-    try {
-      List<String> errs = packageInfo.validate(params.coreContainer);
-      if(!errs.isEmpty()){
-        for (String err : errs) op.addError(err);
+
+    boolean deleteReqHandler(CallInfo info) throws Exception {
+      String name = info.command.getStr("");
+      ClusterProperties clusterProperties = new ClusterProperties(coreContainer.getZkController().getZkClient());
+      Map<String, Object> map = clusterProperties.getClusterProperties();
+      if (Utils.getObjectByPath(map, false, asList(SolrRequestHandler.TYPE, name)) == null) {
+        info.command.addError("NO such requestHandler with name :");
         return false;
       }
-    } catch (FileNotFoundException fnfe) {
-      op.addError(fnfe.getMessage());
-      return false;
-
-    } catch (SolrException e) {
-      log.error("Error loading package ", e);
-      op.addError(e.getMessage());
-      return false;
-    }
-
-    Map delta = new LinkedHashMap();
-    Utils.setObjectByPath(delta, pathToLib, packageInfo, true);
-    clusterProperties.setClusterProperties(delta);
-    return true;
-
-  }
-
-  private static boolean checkEnabled(ApiInfo params) {
-    if (!PackageBag.enablePackage) {
-      params.op.addError("node not started with enable.package=true");
+      Map m = new LinkedHashMap();
+      Utils.setObjectByPath(m, asList(SolrRequestHandler.TYPE, name), null, true);
+      clusterProperties.setClusterProperties(m);
       return true;
     }
-    return false;
-  }
 
-  static class ApiInfo {
-    final SolrQueryRequest req;
-    final SolrQueryResponse rsp;
-    final BaseHandlerApiSupport apiHandler;
-    final CommandOperation op;
-    final CoreContainer coreContainer;
-
-    ApiInfo(SolrQueryRequest req, SolrQueryResponse rsp, BaseHandlerApiSupport apiHandler, CommandOperation op, CoreContainer coreContainer) {
-      this.req = req;
-      this.rsp = rsp;
-      this.apiHandler = apiHandler;
-      this.op = op;
-      this.coreContainer = coreContainer;
-    }
   }
 
-
   static class PerNodeCallable extends SolrConfigHandler.PerReplicaCallable {
     private final HttpClient httpClient;
     final String v2Url;
@@ -529,13 +408,4 @@ class ClusterAPI {
 
   }
 
-  static void updateRepository(SolrZkClient client, Map delta) throws KeeperException, InterruptedException {
-    client.atomicUpdate(ZkStateReader.PACKAGE_REPO, zkData -> {
-      if (zkData == null) return Utils.toJSON(delta);
-      Map<String, Object> zkJson = (Map<String, Object>) Utils.fromJSON(zkData);
-      boolean modified = Utils.mergeJson(zkJson, delta);
-      return modified ? Utils.toJSON(zkJson) : null;
-    });
-
-  }
 }
diff --git a/solr/core/src/java/org/apache/solr/handler/admin/CollectionHandlerApi.java b/solr/core/src/java/org/apache/solr/handler/admin/CollectionHandlerApi.java
index 95f30bb..2fb3a9f 100644
--- a/solr/core/src/java/org/apache/solr/handler/admin/CollectionHandlerApi.java
+++ b/solr/core/src/java/org/apache/solr/handler/admin/CollectionHandlerApi.java
@@ -23,6 +23,7 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
+import org.apache.solr.api.Api;
 import org.apache.solr.client.solrj.request.CollectionApiMapping;
 import org.apache.solr.client.solrj.request.CollectionApiMapping.CommandMeta;
 import org.apache.solr.client.solrj.request.CollectionApiMapping.Meta;
@@ -39,13 +40,13 @@ public class CollectionHandlerApi extends BaseHandlerApiSupport {
   private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
 
   final CollectionsHandler handler;
-  static Collection<ApiCommand> apiCommands = createApiMapping();
-
+  final Collection<ApiCommand> apiCommands;
   public CollectionHandlerApi(CollectionsHandler handler) {
     this.handler = handler;
+    apiCommands = createApiMapping();
   }
 
-  private static Collection<ApiCommand> createApiMapping() {
+  private Collection<ApiCommand> createApiMapping() {
 
     //there
     Map<CommandMeta, ApiCommand> apiMapping = new HashMap<>();
@@ -67,17 +68,6 @@ public class CollectionHandlerApi extends BaseHandlerApiSupport {
         }
       }
     }
-    //The following APIs have only V2 implementations
-
-    for (ClusterAPI.Commands api : ClusterAPI.Commands.values()) {
-      apiMapping.put(api.meta(), api );
-    }
-
-    for (Meta meta : Meta.values()) {
-      if (apiMapping.get(meta) == null) {
-        log.error("ERROR_INIT. No corresponding API implementation for : " + meta.commandName);
-      }
-    }
 
     return apiMapping.values();
   }
@@ -95,5 +85,9 @@ public class CollectionHandlerApi extends BaseHandlerApiSupport {
     return apiCommands;
   }
 
+  @Override
+  protected Collection<Api> getV2OnlyApis() {
+    return new ClusterAPI(handler.getCoreContainer()).getAllApis();
+  }
 
 }
diff --git a/solr/core/src/java/org/apache/solr/security/PermissionNameProvider.java b/solr/core/src/java/org/apache/solr/security/PermissionNameProvider.java
index 16b39a4..d2ca0cc 100644
--- a/solr/core/src/java/org/apache/solr/security/PermissionNameProvider.java
+++ b/solr/core/src/java/org/apache/solr/security/PermissionNameProvider.java
@@ -54,6 +54,8 @@ public interface PermissionNameProvider {
     METRICS_HISTORY_READ_PERM("metrics-history-read", null),
     BLOB_READ("blob-read", null),
     BLOB_WRITE("blob-write", null),
+    PKG_EDIT("package-write", null),
+    PKG_READ("package-read", null),
     ALL("all", unmodifiableSet(new HashSet<>(asList("*", null))))
     ;
     final String name;
diff --git a/solr/core/src/test/org/apache/solr/handler/TestPackages.java b/solr/core/src/test/org/apache/solr/handler/TestPackages.java
index c9be737..c377426 100644
--- a/solr/core/src/test/org/apache/solr/handler/TestPackages.java
+++ b/solr/core/src/test/org/apache/solr/handler/TestPackages.java
@@ -52,7 +52,6 @@ import org.apache.solr.cloud.SolrCloudTestCase;
 import org.apache.solr.common.NavigableObject;
 import org.apache.solr.common.cloud.ClusterProperties;
 import org.apache.solr.common.cloud.SolrZkClient;
-import org.apache.solr.common.cloud.ZkStateReader;
 import org.apache.solr.common.params.CommonParams;
 import org.apache.solr.common.params.MapSolrParams;
 import org.apache.solr.common.params.SolrParams;
@@ -172,7 +171,7 @@ public class TestPackages extends SolrCloudTestCase {
             .withPayload(payload)
             .withMethod(SolrRequest.METHOD.POST)
             .build().process(cluster.getSolrClient());
-        fail("Expected error");
+        fail("Error expected");
       } catch (BaseHttpSolrClient.RemoteExecutionException e) {
         assertTrue("actual output : " + Utils.toJSONString(e.getMetaData()), e.getMetaData()._getStr("error/details[0]/errorMessages[0]", "").contains("No such blob: "));
       }
@@ -412,6 +411,7 @@ public class TestPackages extends SolrCloudTestCase {
 
   }
 
+  @Test
   public void testPluginFrompackage() throws Exception {
     String COLLECTION_NAME = "globalLoaderColl";
 
@@ -783,47 +783,6 @@ public class TestPackages extends SolrCloudTestCase {
 
   }
 
-  public void testRepoCRUD() throws Exception{
-    MiniSolrCloudCluster cluster = configureCluster(4)
-        .withJettyConfig(jetty -> jetty.enableV2(true))
-        .addConfig("conf", configset("cloud-minimal"))
-        .configure();
-    try {
-      String payload = "{add : {name : myrepo, url: 'http://localhost/abc' , version : '1.1'}}";
-      new V2Request.Builder("/cluster/repository")
-          .withPayload(payload)
-          .withMethod(SolrRequest.METHOD.POST)
-          .build().process(cluster.getSolrClient());
-      Map repojson = Utils.getJson(cluster.getZkClient(), ZkStateReader.PACKAGE_REPO, true);
-
-      assertEquals("http://localhost/abc", Utils.getObjectByPath(repojson, true, "/repository/myrepo/url"));
-      assertEquals("1.1", Utils.getObjectByPath(repojson, true, "/repository/myrepo/version"));
-      payload = "{update : {name : myrepo, url: 'http://localhost/abc' , version : '1.2'}}";
-    new V2Request.Builder("/cluster/repository")
-          .withPayload(payload)
-          .withMethod(SolrRequest.METHOD.POST)
-          .build().process(cluster.getSolrClient());
-      repojson = Utils.getJson(cluster.getZkClient(), ZkStateReader.PACKAGE_REPO, true);
-
-      assertEquals("http://localhost/abc", Utils.getObjectByPath(repojson, true, "/repository/myrepo/url"));
-      assertEquals("1.2", Utils.getObjectByPath(repojson, true, "/repository/myrepo/version"));
-
-      payload = "{delete :  myrepo}";
-    new V2Request.Builder("/cluster/repository")
-          .withPayload(payload)
-          .withMethod(SolrRequest.METHOD.POST)
-          .build().process(cluster.getSolrClient());
-      repojson = Utils.getJson(cluster.getZkClient(), ZkStateReader.PACKAGE_REPO, true);
-
-      assertNull( Utils.getObjectByPath(repojson, true, "/repository/myrepo"));
-
-
-    }finally {
-      cluster.shutdown();
-    }
-
-  }
-
   private void postBlob(SolrClient client, ByteBuffer blob, String sh256) throws SolrServerException, IOException {
     V2Response rsp = new V2Request.Builder("/cluster/blob")
         .withMethod(SolrRequest.METHOD.POST)
diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/request/CollectionApiMapping.java b/solr/solrj/src/java/org/apache/solr/client/solrj/request/CollectionApiMapping.java
index a532c15..4f0fa2d 100644
--- a/solr/solrj/src/java/org/apache/solr/client/solrj/request/CollectionApiMapping.java
+++ b/solr/solrj/src/java/org/apache/solr/client/solrj/request/CollectionApiMapping.java
@@ -339,11 +339,8 @@ public class CollectionApiMapping {
 
   public enum EndPoint implements V2EndPoint {
     CLUSTER("cluster"),
-    CLUSTER_BLOB("cluster.blob"),
     CLUSTER_ALIASES("cluster.aliases"),
     CLUSTER_CMD("cluster.Commands"),
-    CLUSTER_PKG("cluster.package"),
-    CLUSTER_REPO("cluster.repository"),
     CLUSTER_NODES("cluster.nodes"),
     CLUSTER_CMD_STATUS("cluster.commandstatus"),
     CLUSTER_CMD_STATUS_DELETE("cluster.commandstatus.delete"),
diff --git a/solr/solrj/src/resources/apispec/cluster.package.json b/solr/solrj/src/resources/apispec/cluster.packages.Commands.json
similarity index 96%
rename from solr/solrj/src/resources/apispec/cluster.package.json
rename to solr/solrj/src/resources/apispec/cluster.packages.Commands.json
index d58b50c..b52851e 100644
--- a/solr/solrj/src/resources/apispec/cluster.package.json
+++ b/solr/solrj/src/resources/apispec/cluster.packages.Commands.json
@@ -1,6 +1,6 @@
 {
   "methods": [
-    "GET", "POST"
+     "POST"
   ],
   "url": {
     "paths": [
diff --git a/solr/solrj/src/resources/apispec/cluster.packages.GET.json b/solr/solrj/src/resources/apispec/cluster.packages.GET.json
new file mode 100644
index 0000000..ba11bd2
--- /dev/null
+++ b/solr/solrj/src/resources/apispec/cluster.packages.GET.json
@@ -0,0 +1,10 @@
+{
+  "methods": [
+    "GET"
+  ],
+  "url": {
+    "paths": [
+      "/cluster/package"
+    ]
+  }
+}
\ No newline at end of file