blob: 72b59a8c5e275afd70395fd64245afeb43cc7197 [file] [log] [blame]
/*
* 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));
}
}