| /* |
| * 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; |
| |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.nio.ByteBuffer; |
| import java.util.Map; |
| import java.util.concurrent.Callable; |
| |
| import com.google.common.collect.ImmutableMap; |
| import org.apache.commons.io.IOUtils; |
| import org.apache.lucene.util.ResourceLoader; |
| import org.apache.lucene.util.ResourceLoaderAware; |
| import org.apache.solr.api.Command; |
| import org.apache.solr.api.EndPoint; |
| import org.apache.solr.client.solrj.SolrClient; |
| import org.apache.solr.client.solrj.SolrServerException; |
| import org.apache.solr.client.solrj.embedded.JettySolrRunner; |
| import org.apache.solr.client.solrj.impl.BaseHttpSolrClient.RemoteExecutionException; |
| import org.apache.solr.client.solrj.request.V2Request; |
| import org.apache.solr.client.solrj.request.beans.Package; |
| import org.apache.solr.client.solrj.request.beans.PluginMeta; |
| import org.apache.solr.client.solrj.response.V2Response; |
| import org.apache.solr.cloud.ClusterSingleton; |
| import org.apache.solr.cloud.MiniSolrCloudCluster; |
| import org.apache.solr.cloud.SolrCloudTestCase; |
| import org.apache.solr.common.NavigableObject; |
| import org.apache.solr.common.util.Utils; |
| import org.apache.solr.core.CoreContainer; |
| import org.apache.solr.core.SolrResourceLoader; |
| import org.apache.solr.filestore.PackageStoreAPI; |
| import org.apache.solr.filestore.TestDistribPackageStore; |
| import org.apache.solr.filestore.TestDistribPackageStore.Fetcher; |
| import org.apache.solr.pkg.TestPackages; |
| import org.apache.solr.request.SolrQueryRequest; |
| import org.apache.solr.response.SolrQueryResponse; |
| import org.apache.solr.security.PermissionNameProvider; |
| import org.junit.After; |
| import org.junit.Before; |
| import org.junit.Test; |
| |
| import static java.util.Collections.singletonList; |
| import static java.util.Collections.singletonMap; |
| import static org.apache.solr.client.solrj.SolrRequest.METHOD.GET; |
| import static org.apache.solr.filestore.TestDistribPackageStore.readFile; |
| import static org.apache.solr.filestore.TestDistribPackageStore.uploadKey; |
| |
| public class TestContainerPlugin extends SolrCloudTestCase { |
| |
| @Before |
| public void setup() { |
| System.setProperty("enable.packages", "true"); |
| } |
| |
| @After |
| public void teardown() { |
| System.clearProperty("enable.packages"); |
| } |
| |
| @Test |
| public void testApi() throws Exception { |
| MiniSolrCloudCluster cluster = |
| configureCluster(4) |
| .withJettyConfig(jetty -> jetty.enableV2(true)) |
| .configure(); |
| String errPath = "/error/details[0]/errorMessages[0]"; |
| try { |
| PluginMeta plugin = new PluginMeta(); |
| plugin.name = "testplugin"; |
| plugin.klass = C2.class.getName(); |
| //test with an invalid class |
| V2Request req = new V2Request.Builder("/cluster/plugin") |
| .forceV2(true) |
| .POST() |
| .withPayload(singletonMap("add", plugin)) |
| .build(); |
| expectError(req, cluster.getSolrClient(), errPath, "No method with @Command in class"); |
| |
| //test with an invalid class |
| // XXX (ab) in order to support ClusterSingleton we allow adding |
| // plugins without Api EndPoints |
| |
| // plugin.klass = C1.class.getName(); |
| // expectError(req, cluster.getSolrClient(), errPath, "No @EndPoints"); |
| |
| //test with a valid class. This should succeed now |
| plugin.klass = C3.class.getName(); |
| req.process(cluster.getSolrClient()); |
| |
| //just check if the plugin is indeed registered |
| V2Request readPluginState = new V2Request.Builder("/cluster/plugin") |
| .forceV2(true) |
| .GET() |
| .build(); |
| V2Response rsp = readPluginState.process(cluster.getSolrClient()); |
| assertEquals(C3.class.getName(), rsp._getStr("/plugin/testplugin/class", null)); |
| |
| //let's test the plugin |
| TestDistribPackageStore.assertResponseValues(10, |
| () -> new V2Request.Builder("/plugin/my/plugin") |
| .forceV2(true) |
| .GET() |
| .build().process(cluster.getSolrClient()), |
| ImmutableMap.of("/testkey", "testval")); |
| |
| //now remove the plugin |
| new V2Request.Builder("/cluster/plugin") |
| .POST() |
| .forceV2(true) |
| .withPayload("{remove : testplugin}") |
| .build() |
| .process(cluster.getSolrClient()); |
| |
| //verify it is removed |
| rsp = readPluginState.process(cluster.getSolrClient()); |
| assertEquals(null, rsp._get("/plugin/testplugin/class", null)); |
| |
| //test with a class @EndPoint methods. This also uses a template in the path name |
| plugin.klass = C4.class.getName(); |
| plugin.name = "collections"; |
| plugin.pathPrefix = "collections"; |
| expectError(req, cluster.getSolrClient(), errPath, "path must not have a prefix: collections"); |
| |
| plugin.name = "my-random-name"; |
| plugin.pathPrefix = "my-random-prefix"; |
| |
| req.process(cluster.getSolrClient()); |
| |
| //let's test the plugin |
| TestDistribPackageStore.assertResponseValues(10, |
| () -> new V2Request.Builder("/my-random-name/my/plugin") |
| .forceV2(true) |
| .GET() |
| .build().process(cluster.getSolrClient()), |
| ImmutableMap.of("/method.name", "m1")); |
| |
| TestDistribPackageStore.assertResponseValues(10, |
| () -> new V2Request.Builder("/my-random-prefix/their/plugin") |
| .forceV2(true) |
| .GET() |
| .build().process(cluster.getSolrClient()), |
| ImmutableMap.of("/method.name", "m2")); |
| //now remove the plugin |
| new V2Request.Builder("/cluster/plugin") |
| .POST() |
| .forceV2(true) |
| .withPayload("{remove : my-random-name}") |
| .build() |
| .process(cluster.getSolrClient()); |
| |
| expectFail( () -> new V2Request.Builder("/my-random-prefix/their/plugin") |
| .forceV2(true) |
| .GET() |
| .build() |
| .process(cluster.getSolrClient())); |
| expectFail(() -> new V2Request.Builder("/my-random-prefix/their/plugin") |
| .forceV2(true) |
| .GET() |
| .build() |
| .process(cluster.getSolrClient())); |
| |
| // test ClusterSingleton plugin |
| plugin.name = "clusterSingleton"; |
| plugin.klass = C6.class.getName(); |
| req.process(cluster.getSolrClient()); |
| |
| //just check if the plugin is indeed registered |
| readPluginState = new V2Request.Builder("/cluster/plugin") |
| .forceV2(true) |
| .GET() |
| .build(); |
| rsp = readPluginState.process(cluster.getSolrClient()); |
| assertEquals(C6.class.getName(), rsp._getStr("/plugin/clusterSingleton/class", null)); |
| |
| assertTrue("ccProvided", C6.ccProvided); |
| assertTrue("startCalled", C6.startCalled); |
| assertFalse("stopCalled", C6.stopCalled); |
| // kill the Overseer leader |
| for (JettySolrRunner jetty : cluster.getJettySolrRunners()) { |
| if (!jetty.getCoreContainer().getZkController().getOverseer().isClosed()) { |
| cluster.stopJettySolrRunner(jetty); |
| cluster.waitForJettyToStop(jetty); |
| } |
| } |
| assertTrue("stopCalled", C6.stopCalled); |
| } finally { |
| cluster.shutdown(); |
| } |
| } |
| |
| private void expectFail(ThrowingRunnable runnable) throws Exception { |
| for(int i=0;i< 20;i++) { |
| try { |
| runnable.run(); |
| } catch (Throwable throwable) { |
| return; |
| } |
| Thread.sleep(100); |
| } |
| fail("should have failed with an exception"); |
| } |
| @Test |
| public void testApiFromPackage() throws Exception { |
| MiniSolrCloudCluster cluster = |
| configureCluster(4) |
| .withJettyConfig(jetty -> jetty.enableV2(true)) |
| .configure(); |
| String FILE1 = "/myplugin/v1.jar"; |
| String FILE2 = "/myplugin/v2.jar"; |
| |
| String errPath = "/error/details[0]/errorMessages[0]"; |
| try { |
| byte[] derFile = readFile("cryptokeys/pub_key512.der"); |
| uploadKey(derFile, PackageStoreAPI.KEYS_DIR+"/pub_key512.der", cluster); |
| TestPackages.postFileAndWait(cluster, "runtimecode/containerplugin.v.1.jar.bin", FILE1, |
| "pmrmWCDafdNpYle2rueAGnU2J6NYlcAey9mkZYbqh+5RdYo2Ln+llLF9voyRj+DDivK9GV1XdtKvD9rgCxlD7Q=="); |
| TestPackages.postFileAndWait(cluster, "runtimecode/containerplugin.v.2.jar.bin", FILE2, |
| "StR3DmqaUSL7qjDOeVEiCqE+ouiZAkW99fsL48F9oWG047o7NGgwwZ36iGgzDC3S2tPaFjRAd9Zg4UK7OZLQzg=="); |
| |
| // We have two versions of the plugin in 2 different jar files. they are already uploaded to the package store |
| Package.AddVersion add = new Package.AddVersion(); |
| add.version = "1.0"; |
| add.pkg = "mypkg"; |
| add.files = singletonList(FILE1); |
| V2Request addPkgVersionReq = new V2Request.Builder("/cluster/package") |
| .forceV2(true) |
| .POST() |
| .withPayload(singletonMap("add", add)) |
| .build(); |
| addPkgVersionReq.process(cluster.getSolrClient()); |
| |
| waitForAllNodesToSync(cluster, "/cluster/package", Utils.makeMap( |
| ":result:packages:mypkg[0]:version", "1.0", |
| ":result:packages:mypkg[0]:files[0]", FILE1 |
| )); |
| |
| // Now lets create a plugin using v1 jar file |
| PluginMeta plugin = new PluginMeta(); |
| plugin.name = "myplugin"; |
| plugin.klass = "mypkg:org.apache.solr.handler.MyPlugin"; |
| plugin.version = add.version; |
| final V2Request req1 = new V2Request.Builder("/cluster/plugin") |
| .forceV2(true) |
| .POST() |
| .withPayload(singletonMap("add", plugin)) |
| .build(); |
| req1.process(cluster.getSolrClient()); |
| //verify the plugin creation |
| TestDistribPackageStore.assertResponseValues(10, |
| () -> new V2Request.Builder("/cluster/plugin") |
| .GET() |
| .build().process(cluster.getSolrClient()), |
| ImmutableMap.of( |
| "/plugin/myplugin/class", plugin.klass, |
| "/plugin/myplugin/version", plugin.version |
| )); |
| //let's test this now |
| Callable<NavigableObject> invokePlugin = () -> new V2Request.Builder("/plugin/my/path") |
| .forceV2(true) |
| .GET() |
| .build().process(cluster.getSolrClient()); |
| TestDistribPackageStore.assertResponseValues(10, |
| invokePlugin, |
| ImmutableMap.of("/myplugin.version", "1.0")); |
| |
| //now let's upload the jar file for version 2.0 of the plugin |
| add.version = "2.0"; |
| add.files = singletonList(FILE2); |
| addPkgVersionReq.process(cluster.getSolrClient()); |
| |
| //here the plugin version is updated |
| plugin.version = add.version; |
| new V2Request.Builder("/cluster/plugin") |
| .forceV2(true) |
| .GET() |
| .withPayload(singletonMap("update", plugin)) |
| .build() |
| .process(cluster.getSolrClient()); |
| |
| //now verify if it is indeed updated |
| TestDistribPackageStore.assertResponseValues(10, |
| () -> new V2Request.Builder("/cluster/plugin") |
| .GET() |
| .build().process(cluster.getSolrClient()), |
| ImmutableMap.of( |
| "/plugin/myplugin/class", plugin.klass, |
| "/plugin/myplugin/version", "2.0" |
| )); |
| // invoke the plugin and test thye output |
| TestDistribPackageStore.assertResponseValues(10, |
| invokePlugin, |
| ImmutableMap.of("/myplugin.version", "2.0")); |
| |
| plugin.name = "plugin2"; |
| plugin.klass = "mypkg:"+ C5.class.getName(); |
| plugin.version = "2.0"; |
| req1.process(cluster.getSolrClient()); |
| assertNotNull(C5.classData); |
| assertEquals( 1452, C5.classData.limit()); |
| } finally { |
| cluster.shutdown(); |
| } |
| } |
| |
| public static class C6 implements ClusterSingleton { |
| static boolean startCalled = false; |
| static boolean stopCalled = false; |
| static boolean ccProvided = false; |
| |
| private State state = State.STOPPED; |
| |
| public C6(CoreContainer cc) { |
| if (cc != null) { |
| ccProvided = true; |
| } |
| } |
| |
| @Override |
| public String getName() { |
| return "C6"; |
| } |
| |
| @Override |
| public void start() throws Exception { |
| state = State.STARTING; |
| startCalled = true; |
| state = State.RUNNING; |
| } |
| |
| @Override |
| public State getState() { |
| return state; |
| } |
| |
| @Override |
| public void stop() { |
| state = State.STOPPING; |
| stopCalled = true; |
| state = State.STOPPED; |
| } |
| } |
| |
| |
| public static class C5 implements ResourceLoaderAware { |
| static ByteBuffer classData; |
| private SolrResourceLoader resourceLoader; |
| |
| @Override |
| @SuppressWarnings("unchecked") |
| public void inform(ResourceLoader loader) throws IOException { |
| this.resourceLoader = (SolrResourceLoader) loader; |
| try { |
| InputStream is = resourceLoader.openResource("org/apache/solr/handler/MyPlugin.class"); |
| byte[] buf = new byte[1024*5]; |
| int sz = IOUtils.read(is, buf); |
| classData = ByteBuffer.wrap(buf, 0,sz); |
| } catch (IOException e) { |
| //do not do anything |
| } |
| } |
| |
| @EndPoint(method = GET, |
| path = "/$plugin-name/m2", |
| permission = PermissionNameProvider.Name.COLL_READ_PERM) |
| public void m2() { |
| |
| |
| } |
| |
| } |
| |
| public static class C1 { |
| |
| } |
| |
| @EndPoint( |
| method = GET, |
| path = "/plugin/my/plugin", |
| permission = PermissionNameProvider.Name.COLL_READ_PERM) |
| public class C2 { |
| |
| |
| } |
| |
| @EndPoint( |
| method = GET, |
| path = "/plugin/my/plugin", |
| permission = PermissionNameProvider.Name.COLL_READ_PERM) |
| public static class C3 { |
| @Command |
| public void read(SolrQueryRequest req, SolrQueryResponse rsp) { |
| rsp.add("testkey", "testval"); |
| } |
| |
| } |
| |
| public static class C4 { |
| |
| @EndPoint(method = GET, |
| path = "$plugin-name/my/plugin", |
| permission = PermissionNameProvider.Name.READ_PERM) |
| public void m1(SolrQueryRequest req, SolrQueryResponse rsp) { |
| rsp.add("method.name", "m1"); |
| } |
| |
| @EndPoint(method = GET, |
| path = "$path-prefix/their/plugin", |
| permission = PermissionNameProvider.Name.READ_PERM) |
| public void m2(SolrQueryRequest req, SolrQueryResponse rsp) { |
| rsp.add("method.name", "m2"); |
| } |
| |
| } |
| |
| @SuppressWarnings("unchecked") |
| public static void waitForAllNodesToSync(MiniSolrCloudCluster cluster, String path, Map<String,Object> expected) throws Exception { |
| for (JettySolrRunner jettySolrRunner : cluster.getJettySolrRunners()) { |
| String baseUrl = jettySolrRunner.getBaseUrl().toString().replace("/solr", "/api"); |
| String url = baseUrl + path + "?wt=javabin"; |
| TestDistribPackageStore.assertResponseValues(10, new Fetcher(url, jettySolrRunner), expected); |
| } |
| } |
| |
| private void expectError(V2Request req, SolrClient client, String errPath, String expectErrorMsg) throws IOException, SolrServerException { |
| RemoteExecutionException e = expectThrows(RemoteExecutionException.class, () -> req.process(client)); |
| String msg = e.getMetaData()._getStr(errPath, ""); |
| assertTrue(expectErrorMsg, msg.contains(expectErrorMsg)); |
| } |
| } |