| /* |
| * 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; |
| } |
| |
| } |