SOLR-13662: Package manager (CLI)

This commit is contained in:
Ishan Chattopadhyaya 2019-11-14 18:21:35 +05:30
parent 3c9140f243
commit 6edbda7429
25 changed files with 2057 additions and 4 deletions

View File

@ -26,6 +26,8 @@ com.fasterxml.jackson.core.version = 2.9.9
/com.github.ben-manes.caffeine/caffeine = 2.8.0
/com.github.virtuald/curvesapi = 1.04
/com.github.zafarkhaja/java-semver = 0.9.0
/com.google.guava/guava = 25.1-jre
/com.google.protobuf/protobuf-java = 3.6.1
/com.google.re2j/re2j = 1.2

View File

@ -39,9 +39,15 @@ New Features
* SOLR-13821: A Package store to store and load package artifacts (noble, Ishan Chattopadhyaya)
* SOLR-13822: A Package management system with the following features. A packages.json in ZK to store
the configuration, APIs to read/edit them and isolated classloaders to load the classes from
hose packages if the 'class' attribute is prefixed with `<package-name>:` (noble, Ishan Chattopadhyaya)
* SOLR-13822: A Package management system with the following features:
(a) A packages.json in ZK to store the configuration,
(b) APIs to read/edit them, and
(c) Isolated classloaders to load the classes from those packages when the 'class'
attribute is prefixed with '<package-name>:'
(noble, Ishan Chattopadhyaya)
* SOLR-13662: A CLI based Package Manager ("bin/solr package help" for more details).
(Ishan Chattopadhyaya, noble, David Smiley, Jan Hoydahl)
* SOLR-10786: Add DBSCAN clustering Streaming Evaluator (Joel Bernstein)

View File

@ -764,6 +764,56 @@ function get_info() {
return $CODE
} # end get_info
function run_package() {
runningSolrUrl=""
numSolrs=`find "$SOLR_PID_DIR" -name "solr-*.pid" -type f | wc -l | tr -d ' '`
if [ "$numSolrs" != "0" ]; then
#echo -e "\nFound $numSolrs Solr nodes: "
while read PIDF
do
ID=`cat "$PIDF"`
port=`jetty_port "$ID"`
if [ "$port" != "" ]; then
#echo -e "\nSolr process $ID running on port $port"
runningSolrUrl="$SOLR_URL_SCHEME://$SOLR_TOOL_HOST:$port/solr"
break
CODE=$?
echo ""
else
echo -e "\nSolr process $ID from $PIDF not found."
CODE=1
fi
done < <(find "$SOLR_PID_DIR" -name "solr-*.pid" -type f)
else
# no pid files but check using ps just to be sure
numSolrs=`ps auxww | grep start\.jar | grep solr\.solr\.home | grep -v grep | wc -l | sed -e 's/^[ \t]*//'`
if [ "$numSolrs" != "0" ]; then
echo -e "\nFound $numSolrs Solr nodes: "
PROCESSES=$(ps auxww | grep start\.jar | grep solr\.solr\.home | grep -v grep | awk '{print $2}' | sort -r)
for ID in $PROCESSES
do
port=`jetty_port "$ID"`
if [ "$port" != "" ]; then
echo ""
echo "Solr process $ID running on port $port"
runningSolrUrl="$SOLR_URL_SCHEME://$SOLR_TOOL_HOST:$port/solr"
break
CODE=$?
echo ""
fi
done
else
echo -e "\nNo Solr nodes are running.\n"
exit 1
CODE=3
fi
fi
run_tool package -solrUrl "$runningSolrUrl" $@
#exit $?
}
# tries to gracefully stop Solr using the Jetty
# stop command and if that fails, then uses kill -9
function stop_solr() {
@ -1359,6 +1409,11 @@ if [[ "$SCRIPT_CMD" == "export" ]]; then
exit $?
fi
if [[ "$SCRIPT_CMD" == "package" ]]; then
run_package $@
exit $?
fi
if [[ "$SCRIPT_CMD" == "auth" ]]; then
VERBOSE=""

View File

@ -215,6 +215,7 @@ IF "%1"=="-version" goto get_version
IF "%1"=="assert" goto run_assert
IF "%1"=="autoscaling" goto run_autoscaling
IF "%1"=="export" goto run_export
IF "%1"=="package" goto run_package
REM Only allow the command to be the first argument, assume start if not supplied
IF "%1"=="start" goto set_script_cmd
@ -1422,6 +1423,15 @@ goto done:
org.apache.solr.util.SolrCLI %*
goto done:
:run_package
REM TODO: Compute the running Solr URL and populate it as a parameter (as has been done for the shell script)
REM Without that, users will have to supply -solrUrl parameter in every request. Life can be so hard for Windows users!
"%JAVA%" %SOLR_SSL_OPTS% %AUTHC_OPTS% %SOLR_ZK_CREDS_AND_ACLS% -Dsolr.install.dir="%SOLR_TIP%" ^
-Dlog4j.configurationFile="file:///%DEFAULT_SERVER_DIR%\resources\log4j2-console.xml" ^
-classpath "%DEFAULT_SERVER_DIR%\solr-webapp\webapp\WEB-INF\lib\*;%DEFAULT_SERVER_DIR%\lib\ext\*" ^
org.apache.solr.util.SolrCLI %*
goto done:
:parse_config_args
IF [%1]==[] goto run_config
IF "%1"=="-z" goto set_config_zk

View File

@ -138,6 +138,9 @@
<dependency org="com.google.protobuf" name="protobuf-java" rev="${/com.google.protobuf/protobuf-java}" conf="compile"/>
<dependency org="com.jayway.jsonpath" name="json-path" rev="${/com.jayway.jsonpath/json-path}" conf="compile"/>
<!-- Package manager -->
<dependency org="com.github.zafarkhaja" name="java-semver" rev="${/com.github.zafarkhaja/java-semver}" conf="compile"/>
<dependency org="org.rrd4j" name="rrd4j" rev="${/org.rrd4j/rrd4j}" conf="compile"/>
<!-- JWT Auth plugin -->

View File

@ -0,0 +1,116 @@
/*
* 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.packagemanager;
import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.io.FileUtils;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.SolrException.ErrorCode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.annotation.JsonIgnore;
/**
* This is a serializable bean (for the JSON that is stored in /repository.json) representing a repository of Solr packages.
* Supports standard repositories based on a webservice.
*/
public class DefaultPackageRepository extends PackageRepository {
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
public DefaultPackageRepository() { // this is needed for deserialization from JSON
}
public DefaultPackageRepository(String repositoryName, String repositoryURL) {
this.name = repositoryName;
this.repositoryURL = repositoryURL;
}
@Override
public void refresh() {
packages = null;
}
@JsonIgnore
private Map<String, SolrPackage> packages;
@Override
public Map<String, SolrPackage> getPackages() {
if (packages == null) {
initPackages();
}
return packages;
}
@Override
public SolrPackage getPackage(String packageName) {
return getPackages().get(packageName);
}
@Override
public boolean hasPackage(String packageName) {
return getPackages().containsKey(packageName);
}
@Override
public Path download(String artifactName) throws SolrException, IOException {
Path tmpDirectory = Files.createTempDirectory("solr-packages");
tmpDirectory.toFile().deleteOnExit();
URL url = new URL(new URL(repositoryURL), artifactName);
String fileName = url.getPath().substring(url.getPath().lastIndexOf('/') + 1);
Path destination = tmpDirectory.resolve(fileName);
switch (url.getProtocol()) {
case "http":
case "https":
case "ftp":
FileUtils.copyURLToFile(url, destination.toFile());
break;
default:
throw new SolrException(ErrorCode.BAD_REQUEST, "URL protocol " + url.getProtocol() + " not supported");
}
return destination;
}
private void initPackages() {
try (CloseableHttpClient client = HttpClientBuilder.create().build()) {
SolrPackage[] items = PackageUtils.getJson(client, repositoryURL + "/repository.json", SolrPackage[].class);
packages = new HashMap<>(items.length);
for (SolrPackage pkg : items) {
pkg.setRepository(name);
packages.put(pkg.name, pkg);
}
} catch (IOException ex) {
throw new SolrException(ErrorCode.INVALID_STATE, ex);
}
log.debug("Found {} packages in repository '{}'", packages.size(), name);
}
}

View File

@ -0,0 +1,416 @@
/*
* 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.packagemanager;
import static org.apache.solr.packagemanager.PackageUtils.getMapper;
import java.io.Closeable;
import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import org.apache.solr.client.solrj.impl.HttpSolrClient;
import org.apache.solr.common.NavigableObject;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.SolrException.ErrorCode;
import org.apache.solr.common.cloud.SolrZkClient;
import org.apache.solr.common.util.Utils;
import org.apache.solr.packagemanager.SolrPackage.Command;
import org.apache.solr.packagemanager.SolrPackage.Manifest;
import org.apache.solr.packagemanager.SolrPackage.Plugin;
import org.apache.solr.pkg.PackagePluginHolder;
import org.apache.solr.util.SolrCLI;
import org.apache.zookeeper.KeeperException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Strings;
import com.jayway.jsonpath.JsonPath;
import com.jayway.jsonpath.PathNotFoundException;
/**
* Handles most of the management of packages that are already installed in Solr.
*/
public class PackageManager implements Closeable {
final String solrBaseUrl;
final HttpSolrClient solrClient;
final SolrZkClient zkClient;
private Map<String, List<SolrPackageInstance>> packages = null;
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
public PackageManager(HttpSolrClient solrClient, String solrBaseUrl, String zkHost) {
this.solrBaseUrl = solrBaseUrl;
this.solrClient = solrClient;
this.zkClient = new SolrZkClient(zkHost, 30000);
log.info("Done initializing a zkClient instance...");
}
@Override
public void close() throws IOException {
if (zkClient != null) {
zkClient.close();
}
}
public List<SolrPackageInstance> fetchInstalledPackageInstances() throws SolrException {
log.info("Getting packages from packages.json...");
List<SolrPackageInstance> ret = new ArrayList<SolrPackageInstance>();
packages = new HashMap<String, List<SolrPackageInstance>>();
try {
Map packagesZnodeMap = null;
if (zkClient.exists("/packages.json", true) == true) {
packagesZnodeMap = (Map)getMapper().readValue(
new String(zkClient.getData("/packages.json", null, null, true), "UTF-8"), Map.class).get("packages");
for (Object packageName: packagesZnodeMap.keySet()) {
List pkg = (List)packagesZnodeMap.get(packageName);
for (Map pkgVersion: (List<Map>)pkg) {
Manifest manifest = PackageUtils.fetchManifest(solrClient, solrBaseUrl, pkgVersion.get("manifest").toString(), pkgVersion.get("manifestSHA512").toString());
List<Plugin> solrplugins = manifest.plugins;
SolrPackageInstance pkgInstance = new SolrPackageInstance(packageName.toString(), null,
pkgVersion.get("version").toString(), manifest, solrplugins, manifest.parameterDefaults);
List<SolrPackageInstance> list = packages.containsKey(packageName)? packages.get(packageName): new ArrayList<SolrPackageInstance>();
list.add(pkgInstance);
packages.put(packageName.toString(), list);
ret.add(pkgInstance);
}
}
}
} catch (Exception e) {
throw new SolrException(ErrorCode.BAD_REQUEST, e);
}
log.info("Got packages: "+ret);
return ret;
}
public Map<String, SolrPackageInstance> getPackagesDeployed(String collection) {
Map<String, String> packages = null;
try {
NavigableObject result = (NavigableObject) Utils.executeGET(solrClient.getHttpClient(),
solrBaseUrl+"/api/collections/"+collection+"/config/params/PKG_VERSIONS?omitHeader=true&wt=javabin", Utils.JAVABINCONSUMER);
packages = (Map<String, String>) result._get("/response/params/PKG_VERSIONS", Collections.emptyMap());
} catch (PathNotFoundException ex) {
// Don't worry if PKG_VERSION wasn't found. It just means this collection was never touched by the package manager.
}
if (packages == null) return Collections.emptyMap();
Map<String, SolrPackageInstance> ret = new HashMap<String, SolrPackageInstance>();
for (String packageName: packages.keySet()) {
if (Strings.isNullOrEmpty(packageName) == false) { // There can be an empty key, storing the version here
ret.put(packageName, getPackageInstance(packageName, packages.get(packageName)));
}
}
return ret;
}
private boolean deployPackage(SolrPackageInstance packageInstance, boolean pegToLatest, boolean isUpdate, boolean noprompt,
List<String> collections, String overrides[]) {
for (String collection: collections) {
SolrPackageInstance deployedPackage = getPackagesDeployed(collection).get(packageInstance.name);
if (packageInstance.equals(deployedPackage)) {
if (!pegToLatest) {
PackageUtils.printRed("Package " + packageInstance + " already deployed on "+collection);
continue;
}
} else {
if (deployedPackage != null && !isUpdate) {
PackageUtils.printRed("Package " + deployedPackage + " already deployed on "+collection+". To update to "+packageInstance+", pass --update parameter.");
continue;
}
}
Map<String,String> collectionParameterOverrides = getCollectionParameterOverrides(packageInstance, isUpdate, overrides, collection);
// Get package params
try {
boolean packageParamsExist = ((Map)PackageUtils.getJson(solrClient.getHttpClient(), solrBaseUrl + "/api/collections/abc/config/params/packages", Map.class)
.getOrDefault("response", Collections.emptyMap())).containsKey("params");
SolrCLI.postJsonToSolr(solrClient, "/api/collections/" + collection + "/config/params",
getMapper().writeValueAsString(Collections.singletonMap(packageParamsExist? "update": "set",
Collections.singletonMap("packages", Collections.singletonMap(packageInstance.name, collectionParameterOverrides)))));
} catch (Exception e) {
throw new SolrException(ErrorCode.SERVER_ERROR, e);
}
// Set the package version in the collection's parameters
try {
SolrCLI.postJsonToSolr(solrClient, "/api/collections/" + collection + "/config/params",
"{set:{PKG_VERSIONS:{" + packageInstance.name+": '" + (pegToLatest? PackagePluginHolder.LATEST: packageInstance.version)+"'}}}");
} catch (Exception ex) {
throw new SolrException(ErrorCode.SERVER_ERROR, ex);
}
// If updating, refresh the package version for this to take effect
if (isUpdate || pegToLatest) {
try {
SolrCLI.postJsonToSolr(solrClient, "/api/cluster/package", "{\"refresh\": \"" + packageInstance.name + "\"}");
} catch (Exception ex) {
throw new SolrException(ErrorCode.SERVER_ERROR, ex);
}
}
// If it is a fresh deploy on a collection, run setup commands all the plugins in the package
if (!isUpdate) {
Map<String, String> systemParams = PackageUtils.map("collection", collection, "package-name", packageInstance.name, "package-version", packageInstance.version);
for (Plugin plugin: packageInstance.plugins) {
Command cmd = plugin.setupCommand;
if (cmd != null && !Strings.isNullOrEmpty(cmd.method)) {
if ("POST".equalsIgnoreCase(cmd.method)) {
try {
String payload = PackageUtils.resolve(getMapper().writeValueAsString(cmd.payload), packageInstance.parameterDefaults, collectionParameterOverrides, systemParams);
String path = PackageUtils.resolve(cmd.path, packageInstance.parameterDefaults, collectionParameterOverrides, systemParams);
PackageUtils.printGreen("Executing " + payload + " for path:" + path);
boolean shouldExecute = true;
if (!noprompt) { // show a prompt asking user to execute the setup command for the plugin
PackageUtils.print(PackageUtils.YELLOW, "Execute this command (y/n): ");
String userInput = new Scanner(System.in, "UTF-8").next();
if (!"yes".equalsIgnoreCase(userInput) && !"y".equalsIgnoreCase(userInput)) {
shouldExecute = false;
PackageUtils.printRed("Skipping setup command for deploying (deployment verification may fail)."
+ " Please run this step manually or refer to package documentation.");
}
}
if (shouldExecute) {
SolrCLI.postJsonToSolr(solrClient, path, payload);
}
} catch (Exception ex) {
throw new SolrException(ErrorCode.SERVER_ERROR, ex);
}
} else {
throw new SolrException(ErrorCode.BAD_REQUEST, "Non-POST method not supported for setup commands");
}
} else {
PackageUtils.printRed("There is no setup command to execute for plugin: " + plugin.name);
}
}
}
// Set the package version in the collection's parameters
try {
SolrCLI.postJsonToSolr(solrClient, "/api/collections/" + collection + "/config/params",
"{update:{PKG_VERSIONS:{'" + packageInstance.name + "' : '" + (pegToLatest? PackagePluginHolder.LATEST: packageInstance.version) + "'}}}");
} catch (Exception ex) {
throw new SolrException(ErrorCode.SERVER_ERROR, ex);
}
}
// Verify that package was successfully deployed
boolean success = verify(packageInstance, collections);
if (success) {
PackageUtils.printGreen("Deployed and verified package: " + packageInstance.name + ", version: " + packageInstance.version);
}
return success;
}
private Map<String,String> getCollectionParameterOverrides(SolrPackageInstance packageInstance, boolean isUpdate,
String[] overrides, String collection) {
Map<String, String> collectionParameterOverrides = isUpdate? getPackageParams(packageInstance.name, collection): new HashMap<String,String>();
if (overrides != null) {
for (String override: overrides) {
collectionParameterOverrides.put(override.split("=")[0], override.split("=")[1]);
}
}
return collectionParameterOverrides;
}
@SuppressWarnings({"rawtypes", "unchecked"})
Map<String, String> getPackageParams(String packageName, String collection) {
try {
return (Map<String, String>)((Map)((Map)((Map)
PackageUtils.getJson(solrClient.getHttpClient(), solrBaseUrl + "/api/collections/" + collection + "/config/params/packages", Map.class)
.get("response"))
.get("params"))
.get("packages")).get(packageName);
} catch (Exception ex) {
// This should be because there are no parameters. Be tolerant here.
return Collections.emptyMap();
}
}
/**
* Given a package and list of collections, verify if the package is installed
* in those collections. It uses the verify command of every plugin in the package (if defined).
*/
public boolean verify(SolrPackageInstance pkg, List<String> collections) {
boolean success = true;
for (Plugin plugin: pkg.plugins) {
PackageUtils.printGreen(plugin.verifyCommand);
for (String collection: collections) {
Map<String, String> collectionParameterOverrides = getPackageParams(pkg.name, collection);
Command cmd = plugin.verifyCommand;
Map<String, String> systemParams = PackageUtils.map("collection", collection, "package-name", pkg.name, "package-version", pkg.version);
String url = solrBaseUrl + PackageUtils.resolve(cmd.path, pkg.parameterDefaults, collectionParameterOverrides, systemParams);
PackageUtils.printGreen("Executing " + url + " for collection:" + collection);
if ("GET".equalsIgnoreCase(cmd.method)) {
String response = PackageUtils.getJsonStringFromUrl(solrClient.getHttpClient(), url);
PackageUtils.printGreen(response);
String actualValue = JsonPath.parse(response, PackageUtils.jsonPathConfiguration())
.read(PackageUtils.resolve(cmd.condition, pkg.parameterDefaults, collectionParameterOverrides, systemParams));
String expectedValue = PackageUtils.resolve(cmd.expected, pkg.parameterDefaults, collectionParameterOverrides, systemParams);
PackageUtils.printGreen("Actual: "+actualValue+", expected: "+expectedValue);
if (!expectedValue.equals(actualValue)) {
PackageUtils.printRed("Failed to deploy plugin: " + plugin.name);
success = false;
}
} else {
throw new SolrException(ErrorCode.BAD_REQUEST, "Non-GET method not supported for setup commands");
}
}
}
return success;
}
/**
* Get the installed instance of a specific version of a package. If version is null, PackageUtils.LATEST or PackagePluginHolder.LATEST,
* then it returns the highest version available in the system for the package.
*/
public SolrPackageInstance getPackageInstance(String packageName, String version) {
fetchInstalledPackageInstances();
List<SolrPackageInstance> versions = packages.get(packageName);
SolrPackageInstance latest = null;
if (versions != null && !versions.isEmpty()) {
latest = versions.get(0);
for (int i=0; i<versions.size(); i++) {
SolrPackageInstance pkg = versions.get(i);
if (pkg.version.equals(version)) {
return pkg;
}
if (PackageUtils.compareVersions(latest.version, pkg.version) <= 0) {
latest = pkg;
}
}
}
if (version == null || version.equalsIgnoreCase(PackageUtils.LATEST) || version.equalsIgnoreCase(PackagePluginHolder.LATEST)) {
return latest;
} else return null;
}
/**
* Deploys a version of a package to a list of collections.
* @param version If null, the most recent version is deployed.
* EXPERT FEATURE: If version is PackageUtils.LATEST, this collection will be auto updated whenever a newer version of this package is installed.
* @param isUpdate Is this a fresh deployment or is it an update (i.e. there is already a version of this package deployed on this collection)
* @param noprompt If true, don't prompt before executing setup commands.
*/
public void deploy(String packageName, String version, String[] collections, String[] parameters,
boolean isUpdate, boolean noprompt) throws SolrException {
boolean pegToLatest = PackageUtils.LATEST.equals(version); // User wants to peg this package's version to the latest installed (for auto-update, i.e. no explicit deploy step)
SolrPackageInstance packageInstance = getPackageInstance(packageName, version);
if (packageInstance == null) {
throw new SolrException(ErrorCode.BAD_REQUEST, "Package instance doesn't exist: " + packageName + ":" + null +
". Use install command to install this version first.");
}
if (version == null) version = packageInstance.version;
Manifest manifest = packageInstance.manifest;
if (PackageUtils.checkVersionConstraint(RepositoryManager.systemVersion, manifest.versionConstraint) == false) {
throw new SolrException(ErrorCode.BAD_REQUEST, "Version incompatible! Solr version: "
+ RepositoryManager.systemVersion + ", package version constraint: " + manifest.versionConstraint);
}
boolean res = deployPackage(packageInstance, pegToLatest, isUpdate, noprompt,
Arrays.asList(collections), parameters);
PackageUtils.print(res? PackageUtils.GREEN: PackageUtils.RED, res? "Deployment successful": "Deployment failed");
}
/**
* Undeploys a packge from given collections.
*/
public void undeploy(String packageName, String[] collections) throws SolrException {
for (String collection: collections) {
SolrPackageInstance deployedPackage = getPackagesDeployed(collection).get(packageName);
Map<String, String> collectionParameterOverrides = getPackageParams(packageName, collection);
// Run the uninstall command for all plugins
Map<String, String> systemParams = PackageUtils.map("collection", collection, "package-name", deployedPackage.name, "package-version", deployedPackage.version);
for (Plugin plugin: deployedPackage.plugins) {
Command cmd = plugin.uninstallCommand;
if (cmd != null && !Strings.isNullOrEmpty(cmd.method)) {
if ("POST".equalsIgnoreCase(cmd.method)) {
try {
String payload = PackageUtils.resolve(getMapper().writeValueAsString(cmd.payload), deployedPackage.parameterDefaults, collectionParameterOverrides, systemParams);
String path = PackageUtils.resolve(cmd.path, deployedPackage.parameterDefaults, collectionParameterOverrides, systemParams);
PackageUtils.printGreen("Executing " + payload + " for path:" + path);
SolrCLI.postJsonToSolr(solrClient, path, payload);
} catch (Exception ex) {
throw new SolrException(ErrorCode.SERVER_ERROR, ex);
}
} else {
throw new SolrException(ErrorCode.BAD_REQUEST, "Non-POST method not supported for uninstall commands");
}
} else {
PackageUtils.printRed("There is no uninstall command to execute for plugin: " + plugin.name);
}
}
// Set the package version in the collection's parameters
try {
SolrCLI.postJsonToSolr(solrClient, "/api/collections/" + collection + "/config/params", "{set: {PKG_VERSIONS: {"+packageName+": null}}}");
SolrCLI.postJsonToSolr(solrClient, "/api/cluster/package", "{\"refresh\": \"" + packageName + "\"}");
} catch (Exception ex) {
throw new SolrException(ErrorCode.SERVER_ERROR, ex);
}
// TODO: Also better to remove the package parameters
}
}
/**
* Given a package, return a map of collections where this package is
* installed to the installed version (which can be {@link PackagePluginHolder#LATEST})
*/
public Map<String, String> getDeployedCollections(String packageName) {
List<String> allCollections;
try {
allCollections = zkClient.getChildren("/collections", null, true);
} catch (KeeperException | InterruptedException e) {
throw new SolrException(ErrorCode.SERVICE_UNAVAILABLE, e);
}
Map<String, String> deployed = new HashMap<String, String>();
for (String collection: allCollections) {
// Check package version installed
String paramsJson = PackageUtils.getJsonStringFromUrl(solrClient.getHttpClient(), solrBaseUrl + "/api/collections/" + collection + "/config/params/PKG_VERSIONS?omitHeader=true");
String version = null;
try {
version = JsonPath.parse(paramsJson, PackageUtils.jsonPathConfiguration())
.read("$['response'].['params'].['PKG_VERSIONS'].['"+packageName+"'])");
} catch (PathNotFoundException ex) {
// Don't worry if PKG_VERSION wasn't found. It just means this collection was never touched by the package manager.
}
if (version != null) {
deployed.put(collection, version);
}
}
return deployed;
}
}

View File

@ -0,0 +1,53 @@
/*
* 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.packagemanager;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Map;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.annotation.JsonProperty;
/**
* Abstract class for a repository, holding {@link SolrPackage} items.
*/
public abstract class PackageRepository {
@JsonProperty("name")
public String name = null;
@JsonProperty("url")
public String repositoryURL = null;
public abstract void refresh();
/**
* Returns a map of package name to {@link SolrPackage}s.
*/
public abstract Map<String, SolrPackage> getPackages();
public abstract SolrPackage getPackage(String packageName);
public abstract boolean hasPackage(String packageName);
/**
* Provides a method to download an artifact from this repository.
*/
public abstract Path download(String artifactName) throws SolrException, IOException;
}

View File

@ -0,0 +1,251 @@
/*
* 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.packagemanager;
import java.io.IOException;
import java.net.MalformedURLException;
import java.nio.ByteBuffer;
import java.nio.file.Path;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import org.apache.commons.io.IOUtils;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.lucene.util.SuppressForbidden;
import org.apache.solr.client.solrj.SolrClient;
import org.apache.solr.client.solrj.SolrRequest;
import org.apache.solr.client.solrj.SolrServerException;
import org.apache.solr.client.solrj.impl.HttpSolrClient;
import org.apache.solr.client.solrj.request.V2Request;
import org.apache.solr.client.solrj.response.V2Response;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.SolrException.ErrorCode;
import org.apache.solr.common.params.CommonParams;
import org.apache.solr.common.params.ModifiableSolrParams;
import org.apache.solr.core.BlobRepository;
import org.apache.solr.packagemanager.SolrPackage.Manifest;
import org.apache.solr.util.SolrJacksonAnnotationInspector;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.zafarkhaja.semver.Version;
import com.google.common.base.Strings;
import com.jayway.jsonpath.Configuration;
import com.jayway.jsonpath.spi.json.JacksonJsonProvider;
import com.jayway.jsonpath.spi.json.JsonProvider;
import com.jayway.jsonpath.spi.mapper.JacksonMappingProvider;
import com.jayway.jsonpath.spi.mapper.MappingProvider;
public class PackageUtils {
/**
* Represents a version which denotes the latest version available at the moment.
*/
public static String LATEST = "latest";
public static Configuration jsonPathConfiguration() {
MappingProvider provider = new JacksonMappingProvider();
JsonProvider jsonProvider = new JacksonJsonProvider();
Configuration c = Configuration.builder().jsonProvider(jsonProvider).mappingProvider(provider).options(com.jayway.jsonpath.Option.REQUIRE_PROPERTIES).build();
return c;
}
public static ObjectMapper getMapper() {
return new ObjectMapper().setAnnotationIntrospector(new SolrJacksonAnnotationInspector());
}
/**
* Uploads a file to the package store / file store of Solr.
*
* @param client A Solr client
* @param buffer File contents
* @param name Name of the file as it will appear in the file store (can be hierarchical)
* @param sig Signature digest (public key should be separately uploaded to ZK)
*/
public static void postFile(SolrClient client, ByteBuffer buffer, String name, String sig)
throws SolrServerException, IOException {
String resource = "/api/cluster/files" + name;
ModifiableSolrParams params = new ModifiableSolrParams();
if (sig != null) {
params.add("sig", sig);
}
V2Response rsp = new V2Request.Builder(resource)
.withMethod(SolrRequest.METHOD.PUT)
.withPayload(buffer)
.forceV2(true)
.withMimeType("application/octet-stream")
.withParams(params)
.build()
.process(client);
if (!name.equals(rsp.getResponse().get(CommonParams.FILE))) {
throw new SolrException(ErrorCode.BAD_REQUEST, "Mismatch in file uploaded. Uploaded: " +
rsp.getResponse().get(CommonParams.FILE)+", Original: "+name);
}
}
/**
* Download JSON from the url and deserialize into klass.
*/
public static <T> T getJson(HttpClient client, String url, Class<T> klass) {
try {
return getMapper().readValue(getJsonStringFromUrl(client, url), klass);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* Search through the list of jar files for a given file. Returns string of
* the file contents or null if file wasn't found. This is suitable for looking
* for manifest or property files within pre-downloaded jar files.
* Please note that the first instance of the file found is returned.
*/
public static String getFileFromJarsAsString(List<Path> jars, String filename) {
for (Path jarfile: jars) {
try (ZipFile zipFile = new ZipFile(jarfile.toFile())) {
ZipEntry entry = zipFile.getEntry(filename);
if (entry == null) continue;
return IOUtils.toString(zipFile.getInputStream(entry), "UTF-8");
} catch (Exception ex) {
throw new SolrException(ErrorCode.BAD_REQUEST, ex);
}
}
return null;
}
/**
* Returns JSON string from a given URL
*/
public static String getJsonStringFromUrl(HttpClient client, String url) {
try {
return IOUtils.toString(client.execute(new HttpGet(url)).getEntity().getContent(), "UTF-8");
} catch (UnsupportedOperationException | IOException e) {
throw new RuntimeException(e);
}
}
/**
* Checks whether a given version satisfies the constraint (defined by a semver expression)
*/
public static boolean checkVersionConstraint(String ver, String constraint) {
return Strings.isNullOrEmpty(constraint) || Version.valueOf(ver).satisfies(constraint);
}
/**
* Fetches a manifest file from the File Store / Package Store. A SHA512 check is enforced after fetching.
*/
public static Manifest fetchManifest(HttpSolrClient solrClient, String solrBaseUrl, String manifestFilePath, String expectedSHA512) throws MalformedURLException, IOException {
String manifestJson = PackageUtils.getJsonStringFromUrl(solrClient.getHttpClient(), solrBaseUrl + "/api/node/files" + manifestFilePath);
String calculatedSHA512 = BlobRepository.sha512Digest(ByteBuffer.wrap(manifestJson.getBytes("UTF-8")));
if (expectedSHA512.equals(calculatedSHA512) == false) {
throw new SolrException(ErrorCode.UNAUTHORIZED, "The manifest SHA512 doesn't match expected SHA512. Possible unauthorized manipulation. "
+ "Expected: " + expectedSHA512 + ", calculated: " + calculatedSHA512 + ", manifest location: " + manifestFilePath);
}
Manifest manifest = getMapper().readValue(manifestJson, Manifest.class);
return manifest;
}
/**
* Replace a templatized string with parameter substituted string. First applies the overrides, then defaults and then systemParams.
*/
public static String resolve(String str, Map<String, String> defaults, Map<String, String> overrides, Map<String, String> systemParams) {
// TODO: Should perhaps use Matchers etc. instead of this clumsy replaceAll().
if (str == null) return null;
for (String param: defaults.keySet()) {
str = str.replaceAll("\\$\\{"+param+"\\}", overrides.containsKey(param)? overrides.get(param): defaults.get(param));
}
for (String param: overrides.keySet()) {
str = str.replaceAll("\\$\\{"+param+"\\}", overrides.get(param));
}
for (String param: systemParams.keySet()) {
str = str.replaceAll("\\$\\{"+param+"\\}", systemParams.get(param));
}
return str;
}
/**
* Compares two versions v1 and v2. Returns negative if v1 isLessThan v2, positive if v1 isGreaterThan v2 and 0 if equal.
*/
public static int compareVersions(String v1, String v2) {
return Version.valueOf(v1).compareTo(Version.valueOf(v2));
}
public static String BLACK = "\u001B[30m";
public static String RED = "\u001B[31m";
public static String GREEN = "\u001B[32m";
public static String YELLOW = "\u001B[33m";
public static String BLUE = "\u001B[34m";
public static String PURPLE = "\u001B[35m";
public static String CYAN = "\u001B[36m";
public static String WHITE = "\u001B[37m";
/**
* Console print using green color
*/
public static void printGreen(Object message) {
PackageUtils.print(PackageUtils.GREEN, message);
}
/**
* Console print using red color
*/
public static void printRed(Object message) {
PackageUtils.print(PackageUtils.RED, message);
}
public static void print(Object message) {
print(null, message);
}
@SuppressForbidden(reason = "Need to use System.out.println() instead of log4j/slf4j for cleaner output")
public static void print(String color, Object message) {
String RESET = "\u001B[0m";
if (color != null) {
System.out.println(color + String.valueOf(message) + RESET);
} else {
System.out.println(message);
}
}
public static String[] validateCollections(String collections[]) {
String collectionNameRegex = "^[a-zA-Z0-9_-]*$";
for (String c: collections) {
if (c.matches(collectionNameRegex) == false) {
throw new SolrException(ErrorCode.BAD_REQUEST, "Invalid collection name: " + c +
". Didn't match the pattern: '"+collectionNameRegex+"'");
}
}
return collections;
}
/**
* Replacement for Java 11's Map.of(). Borrowed from SolrTestCaseJ4's map().
*/
public static Map map(Object... params) {
LinkedHashMap ret = new LinkedHashMap();
for (int i=0; i<params.length; i+=2) {
Object o = ret.put(params[i], params[i+1]);
// TODO: handle multi-valued map?
}
return ret;
}
}

View File

@ -0,0 +1,317 @@
/*
* 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.packagemanager;
import static org.apache.solr.packagemanager.PackageUtils.getMapper;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.lang.invoke.MethodHandles;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.ByteBuffer;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.stream.Collectors;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.lucene.util.Version;
import org.apache.solr.client.solrj.SolrRequest;
import org.apache.solr.client.solrj.SolrServerException;
import org.apache.solr.client.solrj.impl.HttpSolrClient;
import org.apache.solr.client.solrj.request.V2Request;
import org.apache.solr.client.solrj.request.beans.Package;
import org.apache.solr.client.solrj.response.V2Response;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.SolrException.ErrorCode;
import org.apache.solr.common.cloud.SolrZkClient;
import org.apache.solr.core.BlobRepository;
import org.apache.solr.packagemanager.SolrPackage.Artifact;
import org.apache.solr.packagemanager.SolrPackage.SolrPackageRelease;
import org.apache.solr.pkg.PackageAPI;
import org.apache.solr.pkg.PackagePluginHolder;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Handles most of the management of repositories and packages present in external repositories.
*/
public class RepositoryManager {
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
final private PackageManager packageManager;
public static final String systemVersion = Version.LATEST.toString();
final HttpSolrClient solrClient;
public RepositoryManager(HttpSolrClient solrClient, PackageManager packageManager) {
this.packageManager = packageManager;
this.solrClient = solrClient;
}
public List<SolrPackage> getPackages() {
List<SolrPackage> list = new ArrayList<>(getPackagesMap().values());
Collections.sort(list);
return list;
}
/**
* Get a map of package name to {@link SolrPackage} objects
*/
public Map<String, SolrPackage> getPackagesMap() {
Map<String, SolrPackage> packagesMap = new HashMap<>();
for (PackageRepository repository: getRepositories()) {
packagesMap.putAll(repository.getPackages());
}
return packagesMap;
}
/**
* List of added repositories
*/
public List<PackageRepository> getRepositories() {
// TODO: Instead of fetching again and again, we should look for caching this
PackageRepository items[];
try {
items = getMapper().readValue(getRepositoriesJson(packageManager.zkClient), DefaultPackageRepository[].class);
} catch (IOException | KeeperException | InterruptedException e) {
throw new SolrException(ErrorCode.SERVER_ERROR, e);
}
List<PackageRepository> repositories = Arrays.asList(items);
for (PackageRepository updateRepository: repositories) {
updateRepository.refresh();
}
return repositories;
}
/**
* Add a repository to Solr
*/
public void addRepository(String name, String uri) throws KeeperException, InterruptedException, MalformedURLException, IOException {
String existingRepositoriesJson = getRepositoriesJson(packageManager.zkClient);
log.info(existingRepositoriesJson);
List<PackageRepository> repos = getMapper().readValue(existingRepositoriesJson, List.class);
repos.add(new DefaultPackageRepository(name, uri));
if (packageManager.zkClient.exists("/repositories.json", true) == false) {
packageManager.zkClient.create("/repositories.json", getMapper().writeValueAsString(repos).getBytes("UTF-8"), CreateMode.PERSISTENT, true);
} else {
packageManager.zkClient.setData("/repositories.json", getMapper().writeValueAsString(repos).getBytes("UTF-8"), true);
}
if (packageManager.zkClient.exists("/keys", true)==false) packageManager.zkClient.create("/keys", new byte[0], CreateMode.PERSISTENT, true);
if (packageManager.zkClient.exists("/keys/exe", true)==false) packageManager.zkClient.create("/keys/exe", new byte[0], CreateMode.PERSISTENT, true);
if (packageManager.zkClient.exists("/keys/exe/"+name+".der", true)==false) {
packageManager.zkClient.create("/keys/exe/"+name+".der", new byte[0], CreateMode.PERSISTENT, true);
}
packageManager.zkClient.setData("/keys/exe/"+name+".der", IOUtils.toByteArray(new URL(uri+"/publickey.der").openStream()), true);
}
private String getRepositoriesJson(SolrZkClient zkClient) throws UnsupportedEncodingException, KeeperException, InterruptedException {
if (zkClient.exists("/repositories.json", true)) {
return new String(zkClient.getData("/repositories.json", null, null, true), "UTF-8");
}
return "[]";
}
/**
* Install a given package and version from the available repositories to Solr.
* The various steps for doing so are, briefly, a) find upload a manifest to package store,
* b) download the artifacts and upload to package store, c) call {@link PackageAPI} to register
* the package.
*/
private boolean installPackage(String packageName, String version) throws SolrException {
SolrPackageInstance existingPlugin = packageManager.getPackageInstance(packageName, version);
if (existingPlugin != null && existingPlugin.version.equals(version)) {
throw new SolrException(ErrorCode.BAD_REQUEST, "Plugin already installed.");
}
SolrPackageRelease release = getPackageRelease(packageName, version);
List<Path> downloaded = downloadPackageArtifacts(packageName, version);
// TODO: Should we introduce a checksum to validate the downloading?
// Currently, not a big problem since signature based checking happens anyway
try {
// post the manifest
PackageUtils.printGreen("Posting manifest...");
if (release.manifest == null) {
String manifestJson = PackageUtils.getFileFromJarsAsString(downloaded, "manifest.json");
if (manifestJson == null) {
throw new SolrException(ErrorCode.NOT_FOUND, "No manifest found for package: " + packageName + ", version: " + version);
}
release.manifest = getMapper().readValue(manifestJson, SolrPackage.Manifest.class);
}
String manifestJson = getMapper().writeValueAsString(release.manifest);
String manifestSHA512 = BlobRepository.sha512Digest(ByteBuffer.wrap(manifestJson.getBytes("UTF-8")));
PackageUtils.postFile(solrClient, ByteBuffer.wrap(manifestJson.getBytes("UTF-8")),
String.format(Locale.ROOT, "/package/%s/%s/%s", packageName, version, "manifest.json"), null);
// post the artifacts
PackageUtils.printGreen("Posting artifacts...");
for (int i=0; i<release.artifacts.size(); i++) {
PackageUtils.postFile(solrClient, ByteBuffer.wrap(FileUtils.readFileToByteArray(downloaded.get(i).toFile())),
String.format(Locale.ROOT, "/package/%s/%s/%s", packageName, version, downloaded.get(i).getFileName().toString()),
release.artifacts.get(i).sig
);
}
// Call Package API to add this version of the package
PackageUtils.printGreen("Executing Package API to register this package...");
Package.AddVersion add = new Package.AddVersion();
add.version = version;
add.pkg = packageName;
add.files = downloaded.stream().map(
file -> String.format(Locale.ROOT, "/package/%s/%s/%s", packageName, version, file.getFileName().toString())).collect(Collectors.toList());
add.manifest = "/package/" + packageName + "/" + version + "/manifest.json";
add.manifestSHA512 = manifestSHA512;
V2Request req = new V2Request.Builder("/api/cluster/package")
.forceV2(true)
.withMethod(SolrRequest.METHOD.POST)
.withPayload(Collections.singletonMap("add", add))
.build();
try {
V2Response resp = req.process(solrClient);
PackageUtils.printGreen("Response: "+resp.jsonStr());
} catch (SolrServerException | IOException e) {
throw new SolrException(ErrorCode.BAD_REQUEST, e);
}
} catch (SolrServerException | IOException e) {
throw new SolrException(ErrorCode.BAD_REQUEST, e);
}
return false;
}
private List<Path> downloadPackageArtifacts(String packageName, String version) throws SolrException {
try {
SolrPackageRelease release = getPackageRelease(packageName, version);
List<Path> downloadedPaths = new ArrayList<Path>(release.artifacts.size());
for (PackageRepository repo: getRepositories()) {
if (repo.hasPackage(packageName)) {
for (Artifact art: release.artifacts) {
downloadedPaths.add(repo.download(art.url));
}
return downloadedPaths;
}
}
} catch (IOException e) {
throw new SolrException(ErrorCode.SERVER_ERROR, "Error during download of package " + packageName, e);
}
throw new SolrException(ErrorCode.NOT_FOUND, "Package not found in any repository.");
}
/**
* Given a package name and version, find the release/version object as found in the repository
*/
private SolrPackageRelease getPackageRelease(String packageName, String version) throws SolrException {
SolrPackage pkg = getPackagesMap().get(packageName);
if (pkg == null) {
throw new SolrException(ErrorCode.BAD_REQUEST, "Package "+packageName+" not found in any repository");
}
if (version == null || PackageUtils.LATEST.equals(version)) {
return getLastPackageRelease(pkg);
}
for (SolrPackageRelease release : pkg.versions) {
if (PackageUtils.compareVersions(version, release.version) == 0) {
return release;
}
}
throw new SolrException(ErrorCode.BAD_REQUEST, "Package " + packageName + ":" + version + " does not exist in any repository.");
}
public SolrPackageRelease getLastPackageRelease(String packageName) {
SolrPackage pkg = getPackagesMap().get(packageName);
if (pkg == null) {
return null;
}
return getLastPackageRelease(pkg);
}
private SolrPackageRelease getLastPackageRelease(SolrPackage pkg) {
SolrPackageRelease latest = null;
for (SolrPackageRelease release: pkg.versions) {
if (latest == null) {
latest = release;
} else {
if (PackageUtils.compareVersions(latest.version, release.version) < 0) {
latest = release;
}
}
}
return latest;
}
/**
* Is there a version of the package available in the repositories that is more
* latest than our latest installed version of the package?
*/
public boolean hasPackageUpdate(String packageName) {
SolrPackage pkg = getPackagesMap().get(packageName);
if (pkg == null) {
return false;
}
String installedVersion = packageManager.getPackageInstance(packageName, null).version;
SolrPackageRelease last = getLastPackageRelease(packageName);
return last != null && PackageUtils.compareVersions(last.version, installedVersion) > 0;
}
/**
* Install a version of the package. Also, run verify commands in case some
* collection was using {@link PackagePluginHolder#LATEST} version of this package and got auto-updated.
*/
public void install(String packageName, String version) throws SolrException {
String latestVersion = getLastPackageRelease(packageName).version;
Map<String, String> collectionsDeployedIn = packageManager.getDeployedCollections(packageName);
List<String> peggedToLatest = collectionsDeployedIn.keySet().stream().
filter(collection -> collectionsDeployedIn.get(collection).equals(PackagePluginHolder.LATEST)).collect(Collectors.toList());
if (!peggedToLatest.isEmpty()) {
PackageUtils.printGreen("Collections that will be affected (since they are configured to use $LATEST): "+peggedToLatest);
}
if (version == null || version.equals(PackageUtils.LATEST)) {
installPackage(packageName, latestVersion);
} else {
installPackage(packageName, version);
}
SolrPackageInstance updatedPackage = packageManager.getPackageInstance(packageName, PackageUtils.LATEST);
boolean res = packageManager.verify(updatedPackage, peggedToLatest);
PackageUtils.printGreen("Verifying version " + updatedPackage.version +
" on " + peggedToLatest + ", result: " + res);
if (!res) throw new SolrException(ErrorCode.BAD_REQUEST, "Failed verification after deployment");
}
}

View File

@ -0,0 +1,140 @@
/*
* 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.packagemanager;
import java.util.Date;
import java.util.List;
import java.util.Map;
import org.apache.solr.common.annotation.JsonProperty;
import org.apache.solr.common.util.ReflectMapWriter;
/**
* Describes a package (along with all released versions) as it appears in a repository.
*/
public class SolrPackage implements Comparable<SolrPackage>, ReflectMapWriter {
@JsonProperty("name")
public String name;
@JsonProperty("description")
public String description;
@JsonProperty("versions")
public List<SolrPackageRelease> versions;
@JsonProperty("repository")
private String repository;
@Override
public String toString() {
return jsonStr();
}
public static class SolrPackageRelease implements ReflectMapWriter {
@JsonProperty("version")
public String version;
@JsonProperty("date")
public Date date;
@JsonProperty("artifacts")
public List<Artifact> artifacts;
@JsonProperty("manifest")
public Manifest manifest;
@Override
public String toString() {
return jsonStr();
}
}
public static class Artifact implements ReflectMapWriter {
@JsonProperty("url")
public String url;
@JsonProperty("sig")
public String sig;
}
public static class Manifest implements ReflectMapWriter {
@JsonProperty("version-constraint")
public String versionConstraint;
@JsonProperty("plugins")
public List<Plugin> plugins;
@JsonProperty("parameter-defaults")
public Map<String, String> parameterDefaults;
}
public static class Plugin implements ReflectMapWriter {
public String name;
@JsonProperty("setup-command")
public Command setupCommand;
@JsonProperty("uninstall-command")
public Command uninstallCommand;
@JsonProperty("verify-command")
public Command verifyCommand;
@Override
public String toString() {
return jsonStr();
}
}
@Override
public int compareTo(SolrPackage o) {
return name.compareTo(o.name);
}
public String getRepository() {
return repository;
}
public void setRepository(String repository) {
this.repository = repository;
}
public static class Command implements ReflectMapWriter {
@JsonProperty("path")
public String path;
@JsonProperty("method")
public String method;
@JsonProperty("payload")
public Map<String, Object> payload;
@JsonProperty("condition")
public String condition;
@JsonProperty("expected")
public String expected;
@Override
public String toString() {
return jsonStr();
}
}
}

View File

@ -0,0 +1,66 @@
/*
* 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.packagemanager;
import java.util.List;
import java.util.Map;
import org.apache.solr.common.annotation.JsonProperty;
import org.apache.solr.common.util.ReflectMapWriter;
import org.apache.solr.packagemanager.SolrPackage.Manifest;
import org.apache.solr.packagemanager.SolrPackage.Plugin;
/**
* Describes one instance of a package as it exists in Solr when installed.
*/
public class SolrPackageInstance implements ReflectMapWriter {
@JsonProperty("name")
final public String name;
final public String description;
@JsonProperty("version")
final public String version;
final public Manifest manifest;
final public List<Plugin> plugins;
final public Map<String, String> parameterDefaults;
public SolrPackageInstance(String id, String description, String version, Manifest manifest,
List<Plugin> plugins, Map<String, String> params) {
this.name = id;
this.description = description;
this.version = version;
this.manifest = manifest;
this.plugins = plugins;
this.parameterDefaults = params;
}
@Override
public boolean equals(Object obj) {
if (obj == null) return false;
return name.equals(((SolrPackageInstance)obj).name) && version.equals(((SolrPackageInstance)obj).version);
}
@Override
public String toString() {
return jsonStr();
}
}

View File

@ -0,0 +1,21 @@
/*
* 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.
*/
/**
* This package contains Package Manager (CLI) implementation
*/
package org.apache.solr.packagemanager;

View File

@ -180,12 +180,20 @@ public class PackageAPI {
@JsonProperty
public List<String> files;
@JsonProperty
public String manifest;
@JsonProperty
public String manifestSHA512;
public PkgVersion() {
}
public PkgVersion(Package.AddVersion addVersion) {
this.version = addVersion.version;
this.files = addVersion.files;
this.manifest = addVersion.manifest;
this.manifestSHA512 = addVersion.manifestSHA512;
}
@Override

View File

@ -0,0 +1,293 @@
/*
* 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.util;
import static org.apache.solr.packagemanager.PackageUtils.printGreen;
import static org.apache.solr.packagemanager.PackageUtils.print;
import java.lang.invoke.MethodHandles;
import java.util.Map;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.OptionBuilder;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.core.config.Configurator;
import org.apache.lucene.util.SuppressForbidden;
import org.apache.solr.client.solrj.impl.HttpClientUtil;
import org.apache.solr.client.solrj.impl.HttpSolrClient;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.SolrException.ErrorCode;
import org.apache.solr.common.util.Pair;
import org.apache.solr.packagemanager.PackageManager;
import org.apache.solr.packagemanager.PackageUtils;
import org.apache.solr.packagemanager.RepositoryManager;
import org.apache.solr.packagemanager.SolrPackage;
import org.apache.solr.packagemanager.SolrPackage.SolrPackageRelease;
import org.apache.solr.packagemanager.SolrPackageInstance;
import org.apache.solr.util.SolrCLI.StatusTool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class PackageTool extends SolrCLI.ToolBase {
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
@SuppressForbidden(reason = "Need to turn off logging, and SLF4J doesn't seem to provide for a way.")
public PackageTool() {
// Need a logging free, clean output going through to the user.
Configurator.setRootLevel(Level.OFF);
}
@Override
public String getName() {
return "package";
}
public static String solrUrl = null;
public static String solrBaseUrl = null;
public PackageManager packageManager;
public RepositoryManager repositoryManager;
@Override
@SuppressForbidden(reason = "We really need to print the stacktrace here, otherwise "
+ "there shall be little else information to debug problems. Other SolrCLI tools "
+ "don't print stack traces, hence special treatment is needed here.")
protected void runImpl(CommandLine cli) throws Exception {
try {
solrUrl = cli.getOptionValues("solrUrl")[cli.getOptionValues("solrUrl").length-1];
solrBaseUrl = solrUrl.replaceAll("\\/solr$", ""); // strip out ending "/solr"
log.info("Solr url: "+solrUrl+", solr base url: "+solrBaseUrl);
String zkHost = getZkHost(cli);
log.info("ZK: "+zkHost);
String cmd = cli.getArgList().size() == 0? "help": cli.getArgs()[0];
try (HttpSolrClient solrClient = new HttpSolrClient.Builder(solrBaseUrl).build()) {
if (cmd != null) {
packageManager = new PackageManager(solrClient, solrBaseUrl, zkHost);
try {
repositoryManager = new RepositoryManager(solrClient, packageManager);
switch (cmd) {
case "add-repo":
String repoName = cli.getArgs()[1];
String repoUrl = cli.getArgs()[2];
repositoryManager.addRepository(repoName, repoUrl);
PackageUtils.printGreen("Added repository: " + repoName);
break;
case "list-installed":
PackageUtils.printGreen("Installed packages:\n-----");
for (SolrPackageInstance pkg: packageManager.fetchInstalledPackageInstances()) {
PackageUtils.printGreen(pkg);
}
break;
case "list-available":
PackageUtils.printGreen("Available packages:\n-----");
for (SolrPackage pkg: repositoryManager.getPackages()) {
PackageUtils.printGreen(pkg.name + " \t\t"+pkg.description);
for (SolrPackageRelease version: pkg.versions) {
PackageUtils.printGreen("\tVersion: "+version.version);
}
}
break;
case "list-deployed":
if (cli.hasOption('c')) {
String collection = cli.getArgs()[1];
Map<String, SolrPackageInstance> packages = packageManager.getPackagesDeployed(collection);
PackageUtils.printGreen("Packages deployed on " + collection + ":");
for (String packageName: packages.keySet()) {
PackageUtils.printGreen("\t" + packages.get(packageName));
}
} else {
String packageName = cli.getArgs()[1];
Map<String, String> deployedCollections = packageManager.getDeployedCollections(packageName);
PackageUtils.printGreen("Collections on which package " + packageName + " was deployed:");
for (String collection: deployedCollections.keySet()) {
PackageUtils.printGreen("\t" + collection + "("+packageName+":"+deployedCollections.get(collection)+")");
}
}
break;
case "install":
{
Pair<String, String> parsedVersion = parsePackageVersion(cli.getArgList().get(1).toString());
String packageName = parsedVersion.first();
String version = parsedVersion.second();
repositoryManager.install(packageName, version);
PackageUtils.printGreen(repositoryManager.toString() + " installed.");
break;
}
case "deploy":
{
Pair<String, String> parsedVersion = parsePackageVersion(cli.getArgList().get(1).toString());
String packageName = parsedVersion.first();
String version = parsedVersion.second();
boolean noprompt = cli.hasOption('y');
boolean isUpdate = cli.hasOption("update") || cli.hasOption('u');
packageManager.deploy(packageName, version, PackageUtils.validateCollections(cli.getOptionValues("collections")), cli.getOptionValues("param"), isUpdate, noprompt);
break;
}
case "undeploy":
{
Pair<String, String> parsedVersion = parsePackageVersion(cli.getArgList().get(1).toString());
if (parsedVersion.second() != null) {
throw new SolrException(ErrorCode.BAD_REQUEST, "Only package name expected, without a version. Actual: " + cli.getArgList().get(1));
}
String packageName = parsedVersion.first();
packageManager.undeploy(packageName, cli.getOptionValues("collections"));
break;
}
case "help":
case "usage":
print("Package Manager\n---------------");
printGreen("./solr package add-repo <repository-name> <repository-url>");
print("Add a repository to Solr.");
print("");
printGreen("./solr package install <package-name>[:<version>] ");
print("Install a package into Solr. This copies over the artifacts from the repository into Solr's internal package store and sets up classloader for this package to be used.");
print("");
printGreen("./solr package deploy <package-name>[:<version>] [-y] [--update] -collections <comma-separated-collections> [-p <param1>=<val1> -p <param2>=<val2> ...] ");
print("Bootstraps a previously installed package into the specified collections. It the package accepts parameters for its setup commands, they can be specified (as per package documentation).");
print("");
printGreen("./solr package list-installed");
print("Print a list of packages installed in Solr.");
print("");
printGreen("./solr package list-available");
print("Print a list of packages available in the repositories.");
print("");
printGreen("./solr package list-deployed -c <collection>");
print("Print a list of packages deployed on a given collection.");
print("");
printGreen("./solr package list-deployed <package-name>");
print("Print a list of collections on which a given package has been deployed.");
print("");
printGreen("./solr package undeploy <package-name> -collections <comma-separated-collections>");
print("Undeploys a package from specified collection(s)");
print("\n");
print("Note: (a) Please add '-solrUrl http://host:port' parameter if needed (usually on Windows).");
print(" (b) Please make sure that all Solr nodes are started with '-Denable.packages=true' parameter.");
print("\n");
break;
default:
throw new RuntimeException("Unrecognized command: "+cmd);
};
} finally {
packageManager.close();
}
}
}
log.info("Finished: "+cmd);
} catch (Exception ex) {
ex.printStackTrace(); // We need to print this since SolrCLI drops the stack trace in favour of brevity. Package tool should surely print full stacktraces!
throw ex;
}
}
/**
* Parses package name and version in the format "name:version" or "name"
* @return A pair of package name (first) and version (second)
*/
private Pair<String, String> parsePackageVersion(String arg) {
String splits[] = arg.split(":");
if (splits.length > 2) {
throw new SolrException(ErrorCode.BAD_REQUEST, "Invalid package name: " + arg +
". Didn't match the pattern: <packagename>:<version> or <packagename>");
}
String packageName = splits[0];
String version = splits.length == 2? splits[1]: null;
return new Pair(packageName, version);
}
@SuppressWarnings("static-access")
public Option[] getOptions() {
return new Option[] {
OptionBuilder
.withArgName("URL")
.hasArg()
.isRequired(true)
.withDescription("Address of the Solr Web application, defaults to: " + SolrCLI.DEFAULT_SOLR_URL)
.create("solrUrl"),
OptionBuilder
.withArgName("COLLECTIONS")
.hasArgs()
.isRequired(false)
.withDescription("List of collections. Run './solr package help' for more details.")
.create("collections"),
OptionBuilder
.withArgName("PARAMS")
.hasArgs()
.isRequired(false)
.withDescription("List of parameters to be used with deploy command. Run './solr package help' for more details.")
.withLongOpt("param")
.create("p"),
OptionBuilder
.isRequired(false)
.withDescription("If a deployment is an update over a previous deployment. Run './solr package help' for more details.")
.withLongOpt("update")
.create("u"),
OptionBuilder
.isRequired(false)
.withDescription("Run './solr package help' for more details.")
.withLongOpt("collection")
.create("c"),
OptionBuilder
.isRequired(false)
.withDescription("Run './solr package help' for more details.")
.withLongOpt("noprompt")
.create("y")
};
}
private String getZkHost(CommandLine cli) throws Exception {
String zkHost = cli.getOptionValue("zkHost");
if (zkHost != null)
return zkHost;
String systemInfoUrl = solrUrl+"/admin/info/system";
CloseableHttpClient httpClient = SolrCLI.getHttpClient();
try {
// hit Solr to get system info
Map<String,Object> systemInfo = SolrCLI.getJson(httpClient, systemInfoUrl, 2, true);
// convert raw JSON into user-friendly output
StatusTool statusTool = new StatusTool();
Map<String,Object> status = statusTool.reportStatus(solrUrl+"/", systemInfo, httpClient);
Map<String,Object> cloud = (Map<String, Object>)status.get("cloud");
if (cloud != null) {
String zookeeper = (String) cloud.get("ZooKeeper");
if (zookeeper.endsWith("(embedded)")) {
zookeeper = zookeeper.substring(0, zookeeper.length() - "(embedded)".length());
}
zkHost = zookeeper;
}
} finally {
HttpClientUtil.close(httpClient);
}
return zkHost;
}
}

View File

@ -417,6 +417,10 @@ public class SolrCLI {
return new AuthTool();
else if ("autoscaling".equals(toolType))
return new AutoscalingTool();
else if ("export".equals(toolType))
return new ExportTool();
else if ("package".equals(toolType))
return new PackageTool();
// If you add a built-in tool to this class, add it here to avoid
// classpath scanning

View File

@ -0,0 +1,9 @@
-----BEGIN RSA PRIVATE KEY-----
MIIBPAIBAAJBAOkIQ4lxVDPPIR2noh4MrTVw3JASYXd6k0wajrSDFYlwepVoSO/f
LK1AHYomrub8C8dXZwxzPMqpDVQ4MABdgcECAwEAAQJBAKzsLPG43zry4SgYVPzn
e0DE12cxvJHkq5k1u9/HxhuNqUdfhKzkMW5BqayDT74UqSLLTT9zFpGPsRUHD37V
aAECIQD7HV0vP9ipMskCMA+x8H8X/F2V2b6RWGlfIDc6XEiE8QIhAO2Q197gMnzP
z3VluoeL9pORw55aqx7kuAXwqmUqRInRAiBX3wWdpBTX2EqYdmL3nDWNGiVRa5mQ
2MQ+olJRHLvPsQIhAL0w/LmiEpMTbEQyH7qS3GvpScBytJSF0YfpgcnPP4YBAiEA
14io0rN1QgVWykmckeZZSzaPLqpkE2wCUZUOemoBByg=
-----END RSA PRIVATE KEY-----

View File

@ -0,0 +1,56 @@
[
{
"name": "question-answer",
"description": "A natural language question answering plugin",
"versions": [
{
"version": "1.0.0",
"date": "2019-01-01",
"artifacts": [
{
"url": "question-answer-request-handler-1.0.jar",
"sig": "TTzgh5/usbyWg7oZ7lRwz4eQfh1FeXWvv4U85tsVthVz0MRDz9t7SmonDkegZ7OyqeoiQ4I207pifpVW+DRd9Q=="
}
],
"manifest": {
"version-constraint": "8 - 9",
"plugins": [
{
"name": "request-handler",
"setup-command": {
"path": "/api/collections/${collection}/config",
"payload": {"add-requesthandler": {"name": "${RH-HANDLER-PATH}", "class": "question-answer:fullstory.QARequestHandler"}},
"method": "POST"
},
"uninstall-command": {
"path": "/api/collections/${collection}/config",
"payload": {"delete-requesthandler": "${RH-HANDLER-PATH}"},
"method": "POST"
},
"verify-command": {
"path": "/api/collections/${collection}/config/requestHandler?componentName=${RH-HANDLER-PATH}&meta=true",
"method": "GET",
"condition": "$['config'].['requestHandler'].['${RH-HANDLER-PATH}'].['_packageinfo_'].['version']",
"expected": "${package-version}"
}
}
],
"parameter-defaults": {
"RH-HANDLER-PATH": "/mypath"
}
}
},
{
"version": "1.1.0",
"date": "2019-01-01",
"artifacts": [
{
"url": "question-answer-request-handler-1.1.jar",
"sig": "LypqlmbJ76AWa5jx0XhjKxO4lrcQAvpSuYddfzcE6TnX0VDPFhrlQHSSX6cZLtvNbQ+74xUMKgsoX1IUnEnKYw=="
}
]
}
]
}
]

View File

@ -0,0 +1,201 @@
/*
* 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.cloud;
import java.lang.invoke.MethodHandles;
import java.util.Arrays;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.core.config.Configurator;
import org.apache.lucene.util.SuppressForbidden;
import org.apache.solr.client.solrj.request.CollectionAdminRequest;
import org.apache.solr.core.TestSolrConfigHandler;
import org.apache.solr.util.PackageTool;
import org.apache.solr.util.SolrCLI;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.handler.DefaultHandler;
import org.eclipse.jetty.server.handler.HandlerList;
import org.eclipse.jetty.server.handler.ResourceHandler;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class PackageManagerCLITest extends SolrCloudTestCase {
// Note for those who want to modify the jar files used in the packages used in this test:
// You need to re-sign the jars for install step, as follows:
// $ openssl dgst -sha1 -sign ./solr/core/src/test-files/solr/question-answer-repository-private-key.pem ./solr/core/src/test-files/solr/question-answer-repository/question-answer-request-handler-1.1.jar | openssl enc -base64
// You can place the new signature thus obtained (removing any whitespaces) in the repository.json.
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private static LocalWebServer repositoryServer;
@BeforeClass
public static void setupCluster() throws Exception {
System.setProperty("enable.packages", "true");
configureCluster(1)
.addConfig("conf1", TEST_PATH().resolve("configsets").resolve("cloud-minimal").resolve("conf"))
.addConfig("conf2", TEST_PATH().resolve("configsets").resolve("cloud-minimal").resolve("conf"))
.configure();
repositoryServer = new LocalWebServer(TEST_PATH().resolve("question-answer-repository").toString());
repositoryServer.start();
}
@AfterClass
public static void teardown() throws Exception {
repositoryServer.stop();
System.clearProperty("enable.packages");
}
@Test
@SuppressForbidden(reason = "Need to turn off logging, and SLF4J doesn't seem to provide for a way.")
public void testPackageManager() throws Exception {
PackageTool tool = new PackageTool();
// Enable the logger for this test. Need to do this since the tool disables logger.
Configurator.setRootLevel(Level.INFO);
String solrUrl = cluster.getJettySolrRunner(0).getBaseUrl().toString();
run(tool, new String[] {"-solrUrl", solrUrl, "list-installed"});
run(tool, new String[] {"-solrUrl", solrUrl, "add-repo", "fullstory", "http://localhost:" + repositoryServer.getPort()});
run(tool, new String[] {"-solrUrl", solrUrl, "list-available"});
run(tool, new String[] {"-solrUrl", solrUrl, "install", "question-answer:1.0.0"});
run(tool, new String[] {"-solrUrl", solrUrl, "list-installed"});
CollectionAdminRequest.createCollection("abc", "conf1", 1, 1).process(cluster.getSolrClient());
CollectionAdminRequest.createCollection("def", "conf2", 1, 1).process(cluster.getSolrClient());
String rhPath = "/mypath2";
run(tool, new String[] {"-solrUrl", solrUrl, "list-deployed", "question-answer"});
run(tool, new String[] {"-solrUrl", solrUrl, "deploy", "question-answer", "-y", "-collections", "abc", "-p", "RH-HANDLER-PATH=" + rhPath});
assertPackageVersion("abc", "question-answer", "1.0.0", rhPath, "1.0.0");
run(tool, new String[] {"-solrUrl", solrUrl, "list-deployed", "question-answer"});
run(tool, new String[] {"-solrUrl", solrUrl, "list-deployed", "-c", "abc"});
// Should we test the "auto-update to latest" functionality or the default explicit deploy functionality
boolean autoUpdateToLatest = random().nextBoolean();
if (autoUpdateToLatest) {
log.info("Testing auto-update to latest installed");
// This command pegs the version to the latest available
run(tool, new String[] {"-solrUrl", solrUrl, "deploy", "question-answer:latest", "-y", "-collections", "abc"});
assertPackageVersion("abc", "question-answer", "$LATEST", rhPath, "1.0.0");
run(tool, new String[] {"-solrUrl", solrUrl, "install", "question-answer"});
assertPackageVersion("abc", "question-answer", "$LATEST", rhPath, "1.1.0");
} else {
log.info("Testing explicit deployment to a different/newer version");
run(tool, new String[] {"-solrUrl", solrUrl, "install", "question-answer"});
assertPackageVersion("abc", "question-answer", "1.0.0", rhPath, "1.0.0");
if (random().nextBoolean()) { // even if parameters are not passed in, they should be picked up from previous deployment
run(tool, new String[] {"-solrUrl", solrUrl, "deploy", "--update", "-y", "question-answer", "-collections", "abc", "-p", "RH-HANDLER-PATH=" + rhPath});
} else {
run(tool, new String[] {"-solrUrl", solrUrl, "deploy", "--update", "-y", "question-answer", "-collections", "abc"});
}
assertPackageVersion("abc", "question-answer", "1.1.0", rhPath, "1.1.0");
}
log.info("Running undeploy...");
run(tool, new String[] {"-solrUrl", solrUrl, "undeploy", "question-answer", "-collections", "abc"});
run(tool, new String[] {"-solrUrl", solrUrl, "list-deployed", "question-answer"});
}
void assertPackageVersion(String collection, String pkg, String version, String component, String componentVersion) throws Exception {
TestSolrConfigHandler.testForResponseElement(
null,
cluster.getJettySolrRunner(0).getBaseUrl().toString() + "/" + collection,
"/config/params?meta=true",
cluster.getSolrClient(),
Arrays.asList("response", "params", "PKG_VERSIONS", pkg),
version,
1);
TestSolrConfigHandler.testForResponseElement(
null,
cluster.getJettySolrRunner(0).getBaseUrl().toString() + "/" + collection,
"/config/requestHandler?componentName=" + component + "&meta=true",
cluster.getSolrClient(),
Arrays.asList("config", "requestHandler", component, "_packageinfo_", "version"),
componentVersion,
1);
}
private void run(PackageTool tool, String[] args) throws Exception {
int res = tool.runTool(SolrCLI.processCommandLineArgs(SolrCLI.joinCommonAndToolOptions(tool.getOptions()), args));
assertEquals("Non-zero status returned for: " + Arrays.toString(args), 0, res);
}
static class LocalWebServer {
private int port = 0;
final private String resourceDir;
Server server;
ServerConnector connector;
public LocalWebServer(String resourceDir) {
this.resourceDir = resourceDir;
}
public int getPort() {
return connector != null? connector.getLocalPort(): port;
}
public void start() throws Exception {
server = new Server();
connector = new ServerConnector(server);
connector.setPort(port);
server.addConnector(connector);
server.setStopAtShutdown(true);
ResourceHandler resourceHandler = new ResourceHandler();
resourceHandler.setResourceBase(resourceDir);
resourceHandler.setDirectoriesListed(true);
HandlerList handlers = new HandlerList();
handlers.setHandlers(new Handler[] {resourceHandler, new DefaultHandler()});
server.setHandler(handlers);
server.start();
}
public void stop() throws Exception {
server.stop();
}
}
}

View File

@ -0,0 +1 @@
59a83ca73c72a5e25b3f0b1bb305230a11000329

View File

@ -0,0 +1,21 @@
The MIT License
Copyright 2012-2014 Zafar Khaja <zafarkhaja@gmail.com>.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -33,6 +33,10 @@ public class Package {
public String version;
@JsonProperty(required = true)
public List<String> files;
@JsonProperty
public String manifest;
@JsonProperty
public String manifestSHA512;
}