blob: 6f2f618f85d688ec572416c4599c6370df9969f3 [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.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.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Scanner;
import java.util.Set;
import java.util.stream.Collectors;
import org.apache.commons.collections4.MultiValuedMap;
import org.apache.commons.collections4.multimap.HashSetValuedHashMap;
import org.apache.http.client.methods.HttpDelete;
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.request.beans.PluginMeta;
import org.apache.solr.client.solrj.response.V2Response;
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.cloud.ZkStateReader;
import org.apache.solr.common.util.Pair;
import org.apache.solr.common.util.Utils;
import org.apache.solr.filestore.DistribPackageStore;
import org.apache.solr.handler.admin.ContainerPluginsApi;
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.PackageLoader;
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 void uninstall(String packageName, String version) {
SolrPackageInstance packageInstance = getPackageInstance(packageName, version);
if (packageInstance == null) {
PackageUtils.printRed("Package " + packageName + ":" + version + " doesn't exist. Use the install command to install this package version first.");
System.exit(1);
}
// Make sure that this package instance is not deployed on any collection
Map<String, String> collectionsDeployedOn = getDeployedCollections(packageName);
for (String collection: collectionsDeployedOn.keySet()) {
if (version.equals(collectionsDeployedOn.get(collection))) {
PackageUtils.printRed("Package " + packageName + " is currently deployed on collection: " + collection + ". Undeploy the package with undeploy <package-name> -collections <collection1>[,<collection2>,...] before attempting to uninstall the package.");
System.exit(1);
}
}
// Make sure that no plugin from this package instance has been deployed as cluster level plugins
Map<String, SolrPackageInstance> clusterPackages = getPackagesDeployedAsClusterLevelPlugins();
for (String clusterPackageName: clusterPackages.keySet()) {
SolrPackageInstance clusterPackageInstance = clusterPackages.get(clusterPackageName);
if (packageName.equals(clusterPackageName) && version.equals(clusterPackageInstance.version)) {
PackageUtils.printRed("Package " + packageName + "is currently deployed as a cluster-level plugin (" + clusterPackageInstance.getCustomData() + "). Undeploy the package with undeploy <package-name> -collections <collection1>[,<collection2>,...] before uninstalling the package.");
System.exit(1);
}
}
// Delete the package by calling the Package API and remove the Jar
PackageUtils.printGreen("Executing Package API to remove this package...");
Package.DelVersion del = new Package.DelVersion();
del.version = version;
del.pkg = packageName;
V2Request req = new V2Request.Builder(PackageUtils.PACKAGE_PATH)
.forceV2(true)
.withMethod(SolrRequest.METHOD.POST)
.withPayload(Collections.singletonMap("delete", del))
.build();
try {
V2Response resp = req.process(solrClient);
PackageUtils.printGreen("Response: " + resp.jsonStr());
} catch (SolrServerException | IOException e) {
throw new SolrException(ErrorCode.BAD_REQUEST, e);
}
PackageUtils.printGreen("Executing Package Store API to remove the " + packageName + " package...");
List<String> filesToDelete = new ArrayList<>(packageInstance.files);
filesToDelete.add(String.format(Locale.ROOT, "/package/%s/%s/%s", packageName, version, "manifest.json"));
for (String filePath: filesToDelete) {
DistribPackageStore.deleteZKFileEntry(zkClient, filePath);
String path = solrClient.getBaseURL() + "/api/cluster/files" + filePath;
PackageUtils.printGreen("Deleting " + path);
HttpDelete httpDel = new HttpDelete(path);
Utils.executeHttpMethod(solrClient.getHttpClient(), path, Utils.JSONCONSUMER, httpDel);
}
PackageUtils.printGreen("Package uninstalled: " + packageName + ":" + version + ":-)");
}
@SuppressWarnings({"unchecked", "rawtypes"})
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(ZkStateReader.SOLR_PKGS_PATH, true) == true) {
packagesZnodeMap = (Map)getMapper().readValue(
new String(zkClient.getData(ZkStateReader.SOLR_PKGS_PATH, 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);
if (pkgVersion.containsKey("files")) {
pkgInstance.files = (List) pkgVersion.get("files");
}
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;
}
@SuppressWarnings({"unchecked"})
public Map<String, SolrPackageInstance> getPackagesDeployed(String collection) {
Map<String, String> packages = null;
try {
NavigableObject result = (NavigableObject) Utils.executeGET(solrClient.getHttpClient(),
solrBaseUrl + PackageUtils.getCollectionParamsPath(collection) + "/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<>();
for (String packageName: packages.keySet()) {
if (Strings.isNullOrEmpty(packageName) == false && // There can be an empty key, storing the version here
packages.get(packageName) != null) { // null means the package was undeployed from this package before
ret.put(packageName, getPackageInstance(packageName, packages.get(packageName)));
}
}
return ret;
}
/**
* Get a map of packages (key: package name, value: package instance) that have their plugins deployed as cluster level plugins.
* The returned packages also contain the "pluginMeta" from "clusterprops.json" as custom data.
*/
@SuppressWarnings({"unchecked"})
public Map<String, SolrPackageInstance> getPackagesDeployedAsClusterLevelPlugins() {
Map<String, String> packageVersions = new HashMap<>();
MultiValuedMap<String, PluginMeta> packagePlugins = new HashSetValuedHashMap<>(); // map of package name to multiple values of pluginMeta (Map<String, String>)
@SuppressWarnings({"unchecked"})
Map<String, Object> result;
try {
result = (Map<String, Object>) Utils.executeGET(solrClient.getHttpClient(),
solrBaseUrl + PackageUtils.CLUSTERPROPS_PATH, Utils.JSONCONSUMER);
} catch (SolrException ex) {
if (ex.code() == ErrorCode.NOT_FOUND.code) {
result = Collections.emptyMap(); // Cluster props doesn't exist, that means there are no cluster level plugins installed.
} else {
throw ex;
}
}
@SuppressWarnings({"unchecked"})
Map<String, Object> clusterPlugins = (Map<String, Object>) result.getOrDefault(ContainerPluginsApi.PLUGIN, Collections.emptyMap());
for (String key : clusterPlugins.keySet()) {
// Map<String, String> pluginMeta = (Map<String, String>) clusterPlugins.get(key);
PluginMeta pluginMeta;
try {
pluginMeta = PackageUtils.getMapper().readValue(Utils.toJSON(clusterPlugins.get(key)), PluginMeta.class);
} catch (IOException e) {
throw new SolrException(ErrorCode.SERVER_ERROR, "Exception while fetching plugins from /clusterprops.json in ZK.", e);
}
if (pluginMeta.klass.contains(":")) {
String packageName = pluginMeta.klass.substring(0, pluginMeta.klass.indexOf(':'));
packageVersions.put(packageName, pluginMeta.version);
packagePlugins.put(packageName, pluginMeta);
}
}
Map<String, SolrPackageInstance> ret = new HashMap<>();
for (String packageName: packageVersions.keySet()) {
if (Strings.isNullOrEmpty(packageName) == false && // There can be an empty key, storing the version here
packageVersions.get(packageName) != null) { // null means the package was undeployed from this package before
ret.put(packageName, getPackageInstance(packageName, packageVersions.get(packageName)));
ret.get(packageName).setCustomData(packagePlugins.get(packageName));
}
}
return ret;
}
private void ensureCollectionsExist(List<String> collections) {
try {
List<String> existingCollections = zkClient.getChildren("/collections", null, true);
Set<String> nonExistent = new HashSet<>(collections);
nonExistent.removeAll(existingCollections);
if (nonExistent.isEmpty() == false) {
throw new SolrException(ErrorCode.BAD_REQUEST, "Collection(s) doesn't exist: " + nonExistent.toString());
}
} catch (KeeperException | InterruptedException e) {
throw new SolrException(ErrorCode.SERVER_ERROR, "Unable to fetch list of collections from ZK.");
}
}
private boolean deployPackage(SolrPackageInstance packageInstance, boolean pegToLatest, boolean isUpdate, boolean noprompt,
List<String> collections, boolean shouldDeployClusterPlugins, String[] overrides) {
// Install plugins of type "cluster"
boolean clusterSuccess = true;
if (shouldDeployClusterPlugins) {
clusterSuccess = deployClusterPackage(packageInstance, isUpdate, noprompt, overrides);
}
// Install plugins of type "collection"
Pair<List<String>, List<String>> deployResult = deployCollectionPackage(packageInstance, pegToLatest, isUpdate, noprompt, collections, overrides);
List<String> deployedCollections = deployResult.first();
List<String> previouslyDeployedOnCollections = deployResult.second();
// Verify
boolean verifySuccess = true;
// Verify that package was successfully deployed
verifySuccess = verify(packageInstance, deployedCollections, shouldDeployClusterPlugins, overrides);
if (verifySuccess) {
PackageUtils.printGreen("Deployed on " + deployedCollections + " and verified package: " + packageInstance.name + ", version: " + packageInstance.version);
}
return clusterSuccess && previouslyDeployedOnCollections.isEmpty() && verifySuccess;
}
/**
* @return list of collections on which packages deployed on
*/
private Pair<List<String>, List<String>> deployCollectionPackage(SolrPackageInstance packageInstance, boolean pegToLatest, boolean isUpdate,
boolean noprompt, List<String> collections, String[] overrides) {
List<String> previouslyDeployed = new ArrayList<>(); // collections where package is already deployed in
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);
previouslyDeployed.add(collection);
continue;
}
} else {
if (deployedPackage != null && !isUpdate) {
PackageUtils.printRed("Package " + deployedPackage + " already deployed on "+collection+". To update to "+packageInstance+", pass --update parameter.");
previouslyDeployed.add(collection);
continue;
}
}
Map<String,String> collectionParameterOverrides = getCollectionParameterOverrides(packageInstance, isUpdate, overrides, collection);
// Get package params
try {
@SuppressWarnings("unchecked")
boolean packageParamsExist = ((Map<Object, Object>)PackageUtils.getJson(solrClient.getHttpClient(), solrBaseUrl + PackageUtils.getCollectionParamsPath(collection) + "/packages", Map.class)
.getOrDefault("response", Collections.emptyMap())).containsKey("params");
SolrCLI.postJsonToSolr(solrClient, PackageUtils.getCollectionParamsPath(collection),
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, PackageUtils.getCollectionParamsPath(collection),
"{set:{PKG_VERSIONS:{" + packageInstance.name+": '" + (pegToLatest? PackageLoader.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, PackageUtils.PACKAGE_PATH, "{\"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) {
for (Plugin plugin: packageInstance.plugins) {
if ("collection".equalsIgnoreCase(plugin.type) == false || collections.isEmpty()) continue;
Map<String, String> systemParams = Map.of("collection", collection, "package-name", packageInstance.name, "package-version", packageInstance.version, "plugin-name", plugin.name);
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 = prompt(noprompt);
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, PackageUtils.getCollectionParamsPath(collection),
"{update:{PKG_VERSIONS:{'" + packageInstance.name + "' : '" + (pegToLatest? PackageLoader.LATEST: packageInstance.version) + "'}}}");
} catch (Exception ex) {
throw new SolrException(ErrorCode.SERVER_ERROR, ex);
}
}
if (previouslyDeployed.isEmpty() == false) {
PackageUtils.printRed("Already Deployed on " + previouslyDeployed + ", package: " + packageInstance.name + ", version: " + packageInstance.version);
}
List<String> deployedCollections = collections.stream().filter(c -> !previouslyDeployed.contains(c)).collect(Collectors.toList());
return new Pair<List<String>, List<String>>(deployedCollections, previouslyDeployed);
}
@SuppressWarnings("unchecked")
private boolean deployClusterPackage(SolrPackageInstance packageInstance, boolean isUpdate, boolean noprompt, String[] overrides) {
boolean clusterPluginFailed = false;
int numberOfClusterPluginsDeployed = 0;
if (isUpdate) {
for (Plugin plugin: packageInstance.plugins) {
if ("cluster".equalsIgnoreCase(plugin.type) == false) continue;
SolrPackageInstance deployedPackage = getPackagesDeployedAsClusterLevelPlugins().get(packageInstance.name);
if (deployedPackage == null) {
PackageUtils.printRed("Cluster level plugin " + plugin.name + " from package " + packageInstance.name + " not deployed. To deploy, remove the --update parameter.");
clusterPluginFailed = true;
continue;
}
for (PluginMeta pluginMeta: (List<PluginMeta>)deployedPackage.getCustomData()) {
PackageUtils.printGreen("Updating this plugin: " + pluginMeta);
try {
pluginMeta.version = packageInstance.version; // just update the version, let the other metadata same
String postBody = "{\"update\": " + Utils.toJSONString(pluginMeta) + "}";
PackageUtils.printGreen("Posting " + postBody + " to " + PackageUtils.CLUSTER_PLUGINS_PATH);
SolrCLI.postJsonToSolr(solrClient, PackageUtils.CLUSTER_PLUGINS_PATH, postBody);
} catch (Exception e) {
throw new SolrException(ErrorCode.SERVER_ERROR, e);
}
}
numberOfClusterPluginsDeployed++;
}
if (numberOfClusterPluginsDeployed > 0) {
PackageUtils.printGreen(numberOfClusterPluginsDeployed + " cluster level plugins updated.");
} else {
PackageUtils.printRed("No cluster level plugin updated.");
clusterPluginFailed = true;
}
} else {
for (Plugin plugin: packageInstance.plugins) {
if ("cluster".equalsIgnoreCase(plugin.type) == false) continue;
// Check if this cluster level plugin is already deployed
{
Map<String, Object> clusterprops = null;
try {
clusterprops = PackageUtils.getJson(solrClient.getHttpClient(), solrBaseUrl + PackageUtils.CLUSTERPROPS_PATH, Map.class);
} catch (SolrException ex) {
if (ex.code() == ErrorCode.NOT_FOUND.code) {
// Ignore this, as clusterprops may not have been created yet. This means package isn't already installed.
} else throw ex;
}
if (clusterprops != null) {
Object pkg = ((Map<String, Object>)clusterprops.getOrDefault("plugin", Collections.emptyMap())).get(packageInstance.name+":"+plugin.name);
if (pkg != null) {
PackageUtils.printRed("Cluster level plugin " + plugin.name + " from package " + packageInstance.name + " already deployed. To update to " + packageInstance + ", pass --update parameter.");
clusterPluginFailed = true;
continue;
}
}
}
// Lets setup this plugin now
Map<String, String> systemParams = Map.of("package-name", packageInstance.name, "package-version", packageInstance.version, "plugin-name", plugin.name);
Command cmd = plugin.setupCommand;
if (cmd != null && !Strings.isNullOrEmpty(cmd.method)) {
if ("POST".equalsIgnoreCase(cmd.method)) {
try {
Map<String, String> overridesMap = getParameterOverrides(overrides);
String payload = PackageUtils.resolve(getMapper().writeValueAsString(cmd.payload), packageInstance.parameterDefaults, overridesMap, systemParams);
String path = PackageUtils.resolve(cmd.path, packageInstance.parameterDefaults, overridesMap, systemParams);
PackageUtils.printGreen("Executing " + payload + " for path:" + path);
boolean shouldExecute = prompt(noprompt);
if (shouldExecute) {
SolrCLI.postJsonToSolr(solrClient, path, payload);
numberOfClusterPluginsDeployed++;
}
} 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);
}
}
if (numberOfClusterPluginsDeployed > 0) {
PackageUtils.printGreen(numberOfClusterPluginsDeployed + " cluster level plugins setup.");
} else {
PackageUtils.printRed("No cluster level plugin setup.");
clusterPluginFailed = true;
}
}
return !clusterPluginFailed;
}
private boolean prompt(boolean noprompt) {
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. (If you choose no, you can manually deploy/undeploy this plugin later) (y/n): ");
try (Scanner scanner = new Scanner(System.in, "UTF-8")) {
String userInput = scanner.next();
if ("no".trim().equalsIgnoreCase(userInput) || "n".trim().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.");
}
}
}
return shouldExecute;
}
/**
* Parse a map of overrides based on user provided values in format "key1=val1"
*/
private Map<String,String> getParameterOverrides(String[] overrides) {
return getCollectionParameterOverrides(null, false, overrides, null);
}
/**
* Resolve parameter overrides by overlaying provided overrides with collection level overrides already in a deployed package.
*/
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 + PackageUtils.getCollectionParamsPath(collection) + "/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).
*
* @param overrides are needed only when shouldDeployClusterPlugins is true, since collection level plugins will get their overrides from ZK (collection params API)
*/
public boolean verify(SolrPackageInstance pkg, List<String> collections, boolean shouldDeployClusterPlugins, String overrides[]) {
boolean success = true;
for (Plugin plugin: pkg.plugins) {
Command cmd = plugin.verifyCommand;
if (plugin.verifyCommand != null && !Strings.isNullOrEmpty(cmd.path)) {
if ("cluster".equalsIgnoreCase(plugin.type)) {
if (!shouldDeployClusterPlugins) continue; // Plugins of type "cluster"
Map<String, String> overridesMap = getParameterOverrides(overrides);
Map<String, String> systemParams = Map.of("package-name", pkg.name, "package-version", pkg.version, "plugin-name", plugin.name);
String url = solrBaseUrl + PackageUtils.resolve(cmd.path, pkg.parameterDefaults, overridesMap, systemParams);
PackageUtils.printGreen("Executing " + url + " for cluster level plugin");
if ("GET".equalsIgnoreCase(cmd.method)) {
String response = PackageUtils.getJsonStringFromUrl(solrClient.getHttpClient(), url);
PackageUtils.printGreen(response);
String actualValue = null;
try {
actualValue = JsonPath.parse(response, PackageUtils.jsonPathConfiguration())
.read(PackageUtils.resolve(cmd.condition, pkg.parameterDefaults, overridesMap, systemParams));
} catch (PathNotFoundException ex) {
PackageUtils.printRed("Failed to deploy plugin: " + plugin.name);
success = false;
}
if (actualValue != null) {
String expectedValue = PackageUtils.resolve(cmd.expected, pkg.parameterDefaults, overridesMap, 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 verify commands");
}
} else {
// Plugins of type "collection"
for (String collection: collections) {
Map<String, String> collectionParameterOverrides = getPackageParams(pkg.name, collection);
Map<String, String> systemParams = Map.of("collection", collection, "package-name", pkg.name, "package-version", pkg.version, "plugin-name", plugin.name);
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 = null;
try {
actualValue = JsonPath.parse(response, PackageUtils.jsonPathConfiguration())
.read(PackageUtils.resolve(cmd.condition, pkg.parameterDefaults, collectionParameterOverrides, systemParams));
} catch (PathNotFoundException ex) {
PackageUtils.printRed("Failed to deploy plugin: " + plugin.name);
success = false;
}
if (actualValue != null) {
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 verify 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(PackageLoader.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, boolean shouldInstallClusterPlugins, String[] parameters,
boolean isUpdate, boolean noprompt) throws SolrException {
ensureCollectionsExist(Arrays.asList(collections));
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) {
PackageUtils.printRed("Package instance doesn't exist: " + packageName + ":" + version + ". Use install command to install this version first.");
System.exit(1);
}
if (version == null) version = packageInstance.version;
Manifest manifest = packageInstance.manifest;
if (PackageUtils.checkVersionConstraint(RepositoryManager.systemVersion, manifest.versionConstraint) == false) {
PackageUtils.printRed("Version incompatible! Solr version: "
+ RepositoryManager.systemVersion + ", package version constraint: " + manifest.versionConstraint);
System.exit(1);
}
boolean res = deployPackage(packageInstance, pegToLatest, isUpdate, noprompt,
Arrays.asList(collections), shouldInstallClusterPlugins, parameters);
PackageUtils.print(res? PackageUtils.GREEN: PackageUtils.RED, res? "Deployment successful": "Deployment failed");
}
/**
* Undeploys a package from given collections.
*/
public void undeploy(String packageName, String[] collections, boolean shouldUndeployClusterPlugins) throws SolrException {
ensureCollectionsExist(Arrays.asList(collections));
// Undeploy cluster level plugins
if (shouldUndeployClusterPlugins) {
SolrPackageInstance deployedPackage = getPackagesDeployedAsClusterLevelPlugins().get(packageName);
if (deployedPackage == null) {
PackageUtils.printRed("Cluster level plugins from package "+packageName+" not deployed.");
} else {
for (Plugin plugin: deployedPackage.plugins) {
if (!shouldUndeployClusterPlugins || "cluster".equalsIgnoreCase(plugin.type) == false) continue;
Map<String, String> systemParams = Map.of("package-name", deployedPackage.name, "package-version", deployedPackage.version, "plugin-name", plugin.name);
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, Collections.emptyMap(), systemParams);
String path = PackageUtils.resolve(cmd.path, deployedPackage.parameterDefaults, Collections.emptyMap(), 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);
}
}
}
}
// Undeploy collection level plugins
for (String collection: collections) {
SolrPackageInstance deployedPackage = getPackagesDeployed(collection).get(packageName);
if (deployedPackage == null) {
PackageUtils.printRed("Package "+packageName+" not deployed on collection "+collection);
continue;
}
Map<String, String> collectionParameterOverrides = getPackageParams(packageName, collection);
// Run the uninstall command for all plugins
for (Plugin plugin: deployedPackage.plugins) {
if ("collection".equalsIgnoreCase(plugin.type) == false) continue;
Map<String, String> systemParams = Map.of("collection", collection, "package-name", deployedPackage.name, "package-version", deployedPackage.version, "plugin-name", plugin.name);
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, PackageUtils.getCollectionParamsPath(collection),
"{set: {PKG_VERSIONS: {"+packageName+": null}}}"); // Is it better to "unset"? If so, build support in params API for "unset"
SolrCLI.postJsonToSolr(solrClient, PackageUtils.PACKAGE_PATH, "{\"refresh\": \"" + packageName + "\"}");
} catch (Exception ex) {
throw new SolrException(ErrorCode.SERVER_ERROR, ex);
}
// TODO: Also better to remove the package parameters PKG_VERSION etc.
}
}
/**
* Given a package, return a map of collections where this package is
* installed to the installed version (which can be {@link PackageLoader#LATEST})
*/
public Map<String, String> getDeployedCollections(String packageName) {
List<String> allCollections;
try {
allCollections = zkClient.getChildren(ZkStateReader.COLLECTIONS_ZKNODE, 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 + PackageUtils.getCollectionParamsPath(collection) + "/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;
}
}