blob: 041ec62dcb9687b89822fba17d4f503cfab9a62d [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.core;
import java.io.File;
import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.PublicKey;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import com.google.common.collect.ImmutableList;
import org.apache.lucene.analysis.util.ResourceLoader;
import org.apache.solr.cloud.CloudUtil;
import org.apache.solr.common.MapWriter;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.cloud.ClusterPropertiesListener;
import org.apache.solr.common.params.CommonParams;
import org.apache.solr.common.util.Base64;
import org.apache.solr.util.CryptoKeys;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.apache.solr.common.params.CommonParams.NAME;
import static org.apache.solr.common.params.CommonParams.PACKAGES;
import static org.apache.solr.common.params.CommonParams.VERSION;
/**
* This class listens to changes to packages and it also keeps a
* registry of the resource loader instances and the metadata of the package
* The resource loader is supposed to be in sync with the data in Zookeeper
* and it will always have one and only one instance of the resource loader
* per package in a given node. These resource loaders are shared across
* all components in a Solr node
* <p>
* when packages are created/updated new resource loaders are created and if there are
* listeners, they are notified. They can in turn choose to discard old instances of plugins
* loaded from old resource loaders and create new instances if required.
* <p>
* All the resource loaders are loaded from files that exist in the {@link DistribFileStore}
*/
public class PackageBag implements ClusterPropertiesListener {
public static final boolean enablePackage = Boolean.parseBoolean(System.getProperty("enable.package", "false"));
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
final CoreContainer coreContainer;
private Map<String, PackageResourceLoader> pkgs = new HashMap<>();
final PackageListeners listenerRegistry = new PackageListeners();
int myVersion = -1;
public int getZNodeVersion(String pkg) {
PackageResourceLoader p = pkgs.get(pkg);
return p == null ? -1 : p.packageInfo.znodeVersion;
}
public PackageInfo getPackageInfo(String pkg) {
PackageResourceLoader p = pkgs.get(pkg);
return p == null ? null : p.packageInfo;
}
public static class PackageInfo implements MapWriter {
public final String name;
public final String version;
public final List<FileObj> fileObjs;
public final int znodeVersion;
public final String manifest;
public PackageInfo(Map m, int znodeVersion) {
name = (String) m.get(NAME);
version = (String) m.get(VERSION);
manifest = (String) m.get("manifest");
this.znodeVersion = znodeVersion;
Object o = m.get("file");
if (o instanceof Map) {
Map map = (Map) o;
this.fileObjs = ImmutableList.of(new FileObj(map));
} else if (o instanceof List) {
List list = (List) o;
ImmutableList.Builder<FileObj> builder = new ImmutableList.Builder();
for (Object o1 : list) {
if(o1 instanceof Map) {
builder.add(new FileObj(o1));
} else {
throw new RuntimeException("Invalid type for attribute 'files'");
}
}
fileObjs = builder.build();
} else throw new RuntimeException("Invalid type for attribute 'file'");
}
public List<String> validate(CoreContainer coreContainer) throws Exception {
List<String> errors = new ArrayList<>();
if (!enablePackage) {
errors.add("node not started with -Denable.package=true");
return errors;
}
Map<String, byte[]> keys = CloudUtil.getTrustedKeys(
coreContainer.getZkController().getZkClient(), "exe");
if (keys.isEmpty()) {
errors.add("No public keys in ZK : /keys/exe");
return errors;
}
CryptoKeys cryptoKeys = new CryptoKeys(keys);
for (FileObj fileObj : fileObjs) {
if (!fileObj.verifyJar(cryptoKeys, coreContainer)) {
errors.add("Invalid signature for file : " + fileObj.fileObjName);
}
}
return errors;
}
@Override
public void writeMap(EntryWriter ew) throws IOException {
ew.put("name", name);
ew.put("version", version);
ew.putIfNotNull("manifest", manifest);
if (fileObjs.size() == 1) {
ew.put("file", fileObjs.get(0));
} else {
ew.put("files", fileObjs);
}
}
@Override
public boolean equals(Object obj) {
if (obj instanceof PackageInfo) {
PackageInfo that = (PackageInfo) obj;
if (!Objects.equals(this.version, that.version)) return false;
if (this.fileObjs.size() == that.fileObjs.size()) {
for (int i = 0; i < fileObjs.size(); i++) {
if (!Objects.equals(fileObjs.get(i), that.fileObjs.get(i))) {
return false;
}
}
} else {
return false;
}
} else {
return false;
}
return true;
}
PackageResourceLoader createPackage(PackageBag packageBag) {
return new PackageResourceLoader(packageBag, this);
}
public static class FileObj implements MapWriter {
public final DistribFileStore.FileObjName fileObjName;
public final String sig;
public FileObj(Object o) {
if (o instanceof Map) {
Map m = (Map) o;
this.fileObjName = new DistribFileStore.FileObjName((String) m.get(CommonParams.ID));
this.sig = (String) m.get("sig");
} else {
throw new RuntimeException("'file' should be a Object Type");
}
}
@Override
public void writeMap(EntryWriter ew) throws IOException {
ew.put(CommonParams.ID, fileObjName.name());
ew.put("sig", sig);
}
@Override
public boolean equals(Object obj) {
if (obj instanceof FileObj) {
FileObj that = (FileObj) obj;
return Objects.equals(this.fileObjName, that.fileObjName) && Objects.equals(this.sig, that.sig);
} else {
return false;
}
}
public boolean verifyJar(CryptoKeys cryptoKeys, CoreContainer coreContainer) throws IOException {
boolean[] result = new boolean[]{false};
for (Map.Entry<String, PublicKey> e : cryptoKeys.keys.entrySet()) {
coreContainer.getFileStore().readFile(fileObjName.name(), is -> {
try {
if (CryptoKeys.verify(e.getValue(), Base64.base64ToByteArray(sig), is)) result[0] = true;
} catch (Exception ex) {
log.error("Unexpected error in verifying jar", ex);
}
});
}
return result[0];
}
}
}
public static class PackageResourceLoader extends SolrResourceLoader implements MapWriter {
final PackageInfo packageInfo;
@Override
public void writeMap(EntryWriter ew) throws IOException {
packageInfo.writeMap(ew);
}
PackageResourceLoader(PackageBag packageBag, PackageInfo packageInfo) {
super(packageBag.coreContainer.getResourceLoader().getInstancePath(),
packageBag.coreContainer.getResourceLoader().classLoader);
this.packageInfo = packageInfo;
List<URL> fileURLs = new ArrayList<>(packageInfo.fileObjs.size());
for (PackageInfo.FileObj fileObj : packageInfo.fileObjs) {
try {
if (!packageBag.coreContainer.getFileStore().fetchFile(fileObj.fileObjName.name())) {
throw new SolrException(SolrException.ErrorCode.SERVER_ERROR,
"File not available " + fileObj.fileObjName.name());
}
fileURLs.add(new File(packageBag.coreContainer.getFileStore().getFileStorePath().toFile(), fileObj.fileObjName.name()).toURI().toURL());
} catch (MalformedURLException e) {
throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, e);
}
}
addToClassLoader(fileURLs);
}
}
PackageBag(CoreContainer coreContainer) {
this.coreContainer = coreContainer;
}
public <T> T newInstance(String cName, Class<T> expectedType, String pkg) {
PackageResourceLoader p = pkgs.get(pkg);
if (p == null) {
return coreContainer.getResourceLoader().newInstance(cName, expectedType);
} else {
return p.newInstance(cName, expectedType);
}
}
@Override
public boolean onChange(Map<String, Object> properties) {
log.debug("clusterprops.json changed , version {}", coreContainer.getZkController().getZkStateReader().getClusterPropsVersion());
int v = coreContainer.getZkController().getZkStateReader().getClusterPropsVersion();
List<PackageInfo> touchedPackages = updatePackages(properties, v);
if (!touchedPackages.isEmpty()) {
Collection<SolrCore> cores = coreContainer.getCores();
log.info(" {} cores being notified of updated packages : {}",cores.size() ,touchedPackages.stream().map(p -> p.name).collect(Collectors.toList()) );
for (SolrCore core : cores) {
core.getListenerRegistry().packagesUpdated(touchedPackages);
}
listenerRegistry.packagesUpdated(touchedPackages);
}
coreContainer.getContainerRequestHandlers().updateReqHandlers(properties);
myVersion = v;
return false;
}
private List<PackageInfo> updatePackages(Map<String, Object> properties, int ver) {
Map m = (Map) properties.getOrDefault(PACKAGES, Collections.emptyMap());
if (pkgs.isEmpty() && m.isEmpty()) return Collections.emptyList();
Map<String, PackageInfo> reloadPackages = new HashMap<>();
m.forEach((k, v) -> {
if (v instanceof Map) {
PackageInfo info = new PackageInfo((Map) v, ver);
PackageResourceLoader pkg = pkgs.get(k);
if (pkg == null || !pkg.packageInfo.equals(info)) {
reloadPackages.put(info.name, info);
}
}
});
pkgs.forEach((name, aPackage) -> {
if (!m.containsKey(name)) reloadPackages.put(name, null);
});
if (!reloadPackages.isEmpty()) {
List<PackageInfo> touchedPackages = new ArrayList<>();
Map<String, PackageResourceLoader> newPkgs = new HashMap<>(pkgs);
reloadPackages.forEach((s, pkgInfo) -> {
if (pkgInfo == null) {
newPkgs.remove(s);
} else {
newPkgs.put(s, pkgInfo.createPackage(PackageBag.this));
touchedPackages.add(pkgInfo);
}
});
this.pkgs = newPkgs;
return touchedPackages;
}
return Collections.emptyList();
}
public ResourceLoader getResourceLoader(String pkg) {
PackageResourceLoader loader = pkgs.get(pkg);
return loader == null ? coreContainer.getResourceLoader() : loader;
}
}