com.ibm.etcd.client.config.EtcdClusterConfig Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of etcd-java Show documentation
Show all versions of etcd-java Show documentation
etcd3 java client and utilities
/*
* Copyright 2017, 2018 IBM Corp. All Rights Reserved.
*
* Licensed 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 com.ibm.etcd.client.config;
import static com.ibm.etcd.client.KeyUtils.bs;
import static java.nio.charset.StandardCharsets.UTF_8;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.security.cert.CertificateException;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Preconditions;
import com.google.common.base.Throwables;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.collect.Sets;
import com.google.common.io.ByteSource;
import com.google.common.io.Files;
import com.google.gson.Gson;
import com.google.gson.annotations.SerializedName;
import com.google.protobuf.ByteString;
import com.ibm.etcd.client.EtcdClient;
/**
* See etcd-json-schema.md for the json schema for etcd cluster config
*
*/
public class EtcdClusterConfig {
private static final Logger logger = LoggerFactory.getLogger(EtcdClusterConfig.class);
public static final int DEFAULT_MAX_MSG_SIZE = 256 * 1024 * 1024; // 256 MiB
//TODO later make this more configurable/narrow; only really needed for getting large ranges
protected final int maxMessageSize = Integer.getInteger("etcd-java.maxMessageSize",
DEFAULT_MAX_MSG_SIZE);
public enum TlsMode { TLS, PLAINTEXT, AUTO }
Set endpoints;
TlsMode tlsMode;
ByteString user, password;
ByteString rootPrefix; // a.k.a namespace
@Deprecated
String composeDeployment;
ByteSource certificate;
String overrideAuthority;
protected EtcdClusterConfig() {}
public ByteString getRootPrefix() {
return rootPrefix;
}
public Set getEndpoints() {
return endpoints;
}
public EtcdClient getClient() throws IOException, CertificateException {
return getClient(this);
}
private EtcdClient newClient() throws IOException, CertificateException {
List endpointList = new ArrayList<>(endpoints);
EtcdClient.Builder builder = EtcdClient.forEndpoints(endpointList)
.withCredentials(user, password).withImmediateAuth()
.withMaxInboundMessageSize(maxMessageSize);
if (overrideAuthority != null) {
builder.overrideAuthority(overrideAuthority);
}
TlsMode ssl = tlsMode;
if (ssl == TlsMode.AUTO || ssl == null) {
String ep = endpointList.get(0);
ssl = ep.startsWith("https://")
|| (!ep.startsWith("http://") && certificate != null)
? TlsMode.TLS : TlsMode.PLAINTEXT;
}
if (ssl == TlsMode.PLAINTEXT) {
builder.withPlainText();
} else if (composeDeployment != null) {
builder.withTrustManager(new ComposeTrustManagerFactory(composeDeployment,
composeDeployment, certificate));
} else if (certificate != null) {
builder.withCaCert(certificate);
}
if (isShutdown) {
throw new IllegalStateException("shutdown");
}
return builder.build();
}
// mainly for testing
public static EtcdClusterConfig newSimpleConfig(String endpoints, String rootPrefix) {
EtcdClusterConfig config = new EtcdClusterConfig();
config.endpoints = Sets.newHashSet(endpoints.split(","));
config.rootPrefix = bs(rootPrefix);
return config;
}
public static EtcdClusterConfig fromProperties(ByteSource source) throws IOException {
Properties props = new Properties();
try (InputStream in = source.openStream()) {
props.load(in);
}
String epString = props.getProperty("endpoints");
if (epString == null) {
throw new IOException("etcd config must contain endpoints property");
}
EtcdClusterConfig config = new EtcdClusterConfig();
config.endpoints = Sets.newHashSet(epString.split(","));
config.user = bs(props.getProperty("username"));
config.password = bs(props.getProperty("password"));
config.composeDeployment = props.getProperty("compose_deployment");
config.rootPrefix = bs(props.getProperty("root_prefix")); // a.k.a namespace
String tlsMode = props.getProperty("tls_mode");
if (tlsMode != null) {
config.tlsMode = TlsMode.valueOf(tlsMode);
}
String certPath = props.getProperty("certificate_file");
if (certPath != null) {
File certFile = new File(certPath);
if (!certFile.exists()) {
throw new IOException("cant find certificate file: " + certPath);
}
config.certificate = Files.asByteSource(certFile);
}
config.overrideAuthority = props.getProperty("override_authority");
return config;
}
public static EtcdClusterConfig fromJson(ByteSource source, File dir) throws IOException {
JsonConfig jsonConfig;
try (InputStream in = source.openStream()) {
jsonConfig = deserializeJson(in);
}
if (jsonConfig.endpoints == null || jsonConfig.endpoints.trim().isEmpty()) {
throw new IOException("etcd config must contain endpoints property");
}
EtcdClusterConfig config = new EtcdClusterConfig();
config.endpoints = Sets.newHashSet(jsonConfig.endpoints.split(","));
config.user = bs(jsonConfig.user);
config.password = bs(jsonConfig.password);
config.composeDeployment = jsonConfig.composeDeployment;
config.rootPrefix = bs(jsonConfig.rootPrefix);
if (jsonConfig.certificateFile != null) {
File certFile = new File(jsonConfig.certificateFile);
if (dir != null && !certFile.exists()) {
// try same dir as the config file
certFile = new File(dir, jsonConfig.certificateFile);
}
if (certFile.exists()) {
config.certificate = Files.asByteSource(certFile);
} else {
// will fall back to embedded if present
logger.warn("Can't find certificate file: " + jsonConfig.certificateFile);
}
}
if (jsonConfig.certificate != null) {
if (config.certificate != null) {
logger.warn("Ignoring json-embedded cert because file was also provided");
} else {
config.certificate = ByteSource.wrap(jsonConfig.certificate.getBytes(UTF_8));
}
}
config.overrideAuthority = jsonConfig.overrideAuthority;
return config;
}
public static EtcdClusterConfig fromJson(ByteSource source) throws IOException {
return fromJson(source, null);
}
public static EtcdClusterConfig fromJsonFile(String file) throws IOException {
File f = new File(file);
return fromJson(Files.asByteSource(f), f.getParentFile());
}
protected static final String ADDR_STR = "(?:https?://)?(?:[a-zA-Z0-9\\-.]+)(?::\\d+)?";
protected static final Pattern SIMPLE_PATT = Pattern.compile(String
.format("((?:%s)(?:,%s)*)(?:;rootPrefix=(.+))?", ADDR_STR, ADDR_STR));
/**
* @param fileOrSimpleString path to json config file or simple config of the form
* "endpoint1,endpoint2,...;rootPrefix=<prefix>", where ;rootPrefix=<prefix>
* is optional.
* @throws IOException
*/
public static EtcdClusterConfig fromJsonFileOrSimple(String fileOrSimpleString) throws IOException {
File f = new File(fileOrSimpleString);
if (f.exists()) {
return fromJson(Files.asByteSource(f), f.getParentFile());
}
Matcher m = SIMPLE_PATT.matcher(fileOrSimpleString);
if (m.matches()) {
return EtcdClusterConfig.newSimpleConfig(m.group(1), m.group(2));
}
throw new FileNotFoundException("etcd config json file not found: " + f);
}
private static final Cache clientCache = CacheBuilder.newBuilder().weakValues()
.removalListener(rn -> rn.getValue().close()).build();
public static EtcdClient getClient(EtcdClusterConfig config) throws IOException, CertificateException {
try {
return clientCache.get(new CacheKey(config), config::newClient);
} catch (ExecutionException ee) {
Throwables.throwIfInstanceOf(ee.getCause(), IOException.class);
Throwables.throwIfInstanceOf(ee.getCause(), CertificateException.class);
Throwables.throwIfUnchecked(ee.getCause());
throw new RuntimeException(ee.getCause());
}
}
private static volatile boolean isShutdown = false;
/**
* Should generally only be called during JVM shutdown
*/
public static void shutdownAll() {
isShutdown = true;
clientCache.invalidateAll();
}
static class CacheKey {
private final EtcdClusterConfig config;
CacheKey(EtcdClusterConfig config) {
this.config = Preconditions.checkNotNull(config);
}
// NOTE: rootPrefix is currently intentionally excluded
// since it's not used to build the client
@Override
public boolean equals(Object obj) {
if (!(obj instanceof CacheKey)) {
return false;
}
EtcdClusterConfig other = ((CacheKey) obj).config;
return Objects.equals(config.endpoints, other.endpoints)
&& Objects.equals(config.composeDeployment, other.composeDeployment)
&& Objects.equals(config.user, other.user)
&& Objects.equals(config.tlsMode, other.tlsMode);
}
@Override
public int hashCode() {
return Objects.hash(config.endpoints, config.user,
config.composeDeployment, config.tlsMode);
}
}
// ---- json deserialization
private static final Gson gson = new Gson();
private static JsonConfig deserializeJson(InputStream in) {
return gson.fromJson(new InputStreamReader(in, StandardCharsets.UTF_8), JsonConfig.class);
}
static class JsonConfig {
@SerializedName("endpoints")
String endpoints;
@SerializedName("userid")
String user;
@SerializedName("password")
String password;
@SerializedName("root_prefix") // a.k.a namespace
String rootPrefix;
@Deprecated
@SerializedName("compose_deployment")
String composeDeployment;
@SerializedName("certificate")
String certificate;
@SerializedName("certificate_file")
String certificateFile;
@SerializedName("override_authority")
String overrideAuthority;
}
}