Skip to content
This repository has been archived by the owner on Apr 23, 2020. It is now read-only.

Commit

Permalink
Add support for consul shared configuration
Browse files Browse the repository at this point in the history
Add consul as a shared config provider.

Fixes #280
  • Loading branch information
scott-rackspace authored and Scott Kruger committed Apr 12, 2017
1 parent 2e0258b commit d746911
Show file tree
Hide file tree
Showing 10 changed files with 426 additions and 1 deletion.
11 changes: 11 additions & 0 deletions exhibitor-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,11 @@
<artifactId>jersey-json</artifactId>
</dependency>

<dependency>
<groupId>com.orbitz.consul</groupId>
<artifactId>consul-client</artifactId>
</dependency>

<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-test</artifactId>
Expand All @@ -111,5 +116,11 @@
<artifactId>testng</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>com.pszymczyk.consul</groupId>
<artifactId>embedded-consul</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package com.netflix.exhibitor.core.config.consul;

import com.netflix.exhibitor.core.config.*;
import com.orbitz.consul.Consul;
import com.orbitz.consul.KeyValueClient;

import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.Properties;
import java.util.concurrent.TimeUnit;

public class ConsulConfigProvider implements ConfigProvider {
private static final Long DEFAULT_LOCK_TIMEOUT_MS = 5L * 60L * 1000L; // 5 minutes;
private final Consul consul;
private final Properties defaults;
private final String basePath;
private final String versionPath;
private final String propertiesPath;
private final ConsulKvLock lock;
private final String pseudoLockPath;
private final Long lockTimeoutMs;

/**
* @param consul consul client instance for connecting to consul cluster
* @param prefix consul key-value path under which configs are stored
* @param defaults default properties
*/
public ConsulConfigProvider(Consul consul, String prefix, Properties defaults) {
this(consul, prefix, defaults, DEFAULT_LOCK_TIMEOUT_MS);
}

/**
* @param consul consul client instance for connecting to consul cluster
* @param prefix consul key-value path under which configs are stored
* @param defaults default properties
* @param lockTimeoutMs timeout, in milliseconds, for lock acquisition
*/
public ConsulConfigProvider(Consul consul, String prefix, Properties defaults, Long lockTimeoutMs) {
this.consul = consul;
this.defaults = defaults;
this.lockTimeoutMs = lockTimeoutMs;

this.basePath = prefix.endsWith("/") ? prefix : prefix + "/";
this.versionPath = basePath + "version";
this.propertiesPath = basePath + "properties";
this.pseudoLockPath = basePath + "pseudo-locks";

this.lock = new ConsulKvLock(consul, basePath + "lock", "exhibitor");
}

@Override
public void start() throws Exception {
// NOP
}

@Override
public void close() throws IOException {
// NOP
}

@Override
public LoadedInstanceConfig loadConfig() throws Exception {
ConsulVersionedProperties properties;

lock.acquireLock(lockTimeoutMs, TimeUnit.MILLISECONDS);
try {
properties = loadProperties();
}
finally {
lock.releaseLock();
}

PropertyBasedInstanceConfig config = new PropertyBasedInstanceConfig(
properties.getProperties(), defaults);
return new LoadedInstanceConfig(config, properties.getVersion());
}

@Override
public LoadedInstanceConfig storeConfig(ConfigCollection config, long compareVersion) throws Exception {
Long currentVersion = loadProperties().getVersion();
if (currentVersion != compareVersion) {
return null;
}

KeyValueClient kv = consul.keyValueClient();
PropertyBasedInstanceConfig instanceConfig = new PropertyBasedInstanceConfig(config);
StringWriter writer = new StringWriter();
instanceConfig.getProperties().store(writer, "Auto-generated by Exhibitor");

lock.acquireLock(lockTimeoutMs, TimeUnit.MILLISECONDS);
try {
kv.putValue(propertiesPath, writer.toString());
kv.putValue(versionPath, String.valueOf(currentVersion + 1));
}
finally {
lock.releaseLock();
}

return new LoadedInstanceConfig(instanceConfig, currentVersion + 1);
}

@Override
public PseudoLock newPseudoLock() throws Exception {
return new ConsulPseudoLock(consul, pseudoLockPath);
}

private String getString(String path) {
return consul.keyValueClient().getValueAsString(path).orNull();
}

private Long getLong(String path) {
return Long.valueOf(consul.keyValueClient().getValueAsString(path).or("0"));
}

private ConsulVersionedProperties loadProperties() throws Exception {
Long version = getLong(versionPath);

Properties properties = new Properties();
String rawProperties = getString(propertiesPath);
if (rawProperties != null) {
StringReader reader = new StringReader(getString(propertiesPath));
properties.load(reader);
}

return new ConsulVersionedProperties(properties, version);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.netflix.exhibitor.core.config.consul;

import com.google.common.base.Optional;
import com.orbitz.consul.Consul;
import com.orbitz.consul.KeyValueClient;
import com.orbitz.consul.model.kv.Value;
import com.orbitz.consul.model.session.ImmutableSession;
import com.orbitz.consul.option.QueryOptions;

import java.math.BigInteger;
import java.util.concurrent.TimeUnit;

public class ConsulKvLock {
private final Consul consul;
private final String path;
private final String name;

/**
* @param consul consul client instance for connecting to consul cluster
* @param path consul key-value path to lock
* @param name a descriptive name for the lock
*/
public ConsulKvLock(Consul consul, String path, String name) {
this.consul = consul;
this.path = path;
this.name = name;
}

private String createSession() {
final ImmutableSession session = ImmutableSession.builder().name(name).build();
return consul.sessionClient().createSession(session).getId();
}

public boolean acquireLock(long maxWait, TimeUnit unit) {
KeyValueClient kv = consul.keyValueClient();
String sessionId = createSession();

Optional<Value> value = kv.getValue(path);

if (kv.acquireLock(path, sessionId)) {
return true;
}

BigInteger index = BigInteger.valueOf(value.get().getModifyIndex());
kv.getValue(path, QueryOptions.blockMinutes((int) unit.toMinutes(maxWait), index).build());
return kv.acquireLock(path, sessionId);
}

public void releaseLock() {
KeyValueClient kv = consul.keyValueClient();
Optional<Value> value = kv.getValue(path);

if (value.isPresent()) {
Optional<String> session = value.get().getSession();
if (session.isPresent()) {
kv.releaseLock(path, session.get());
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.netflix.exhibitor.core.config.consul;

import com.netflix.exhibitor.core.activity.ActivityLog;
import com.netflix.exhibitor.core.config.PseudoLock;
import com.orbitz.consul.Consul;

import java.util.concurrent.TimeUnit;

public class ConsulPseudoLock implements PseudoLock {

private final ConsulKvLock lock;

public ConsulPseudoLock(Consul consul, String prefix) {
String path = prefix.endsWith("/") ? prefix + "pseudo-lock" : prefix + "/pseudo-lock";
this.lock = new ConsulKvLock(consul, path, "pseudo-lock");
}

@Override
public boolean lock(ActivityLog log, long maxWait, TimeUnit unit) throws Exception {
if (!lock.acquireLock(maxWait, unit)) {
log.add(ActivityLog.Type.ERROR,
String.format("Could not acquire lock within %d ms", unit.toMillis(maxWait)));
return false;
}

return true;
}

@Override
public void unlock() throws Exception {
lock.releaseLock();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.netflix.exhibitor.core.config.consul;

import java.util.Properties;

public class ConsulVersionedProperties {
private final Properties properties;
private final Long version;

public ConsulVersionedProperties(Properties properties, Long version) {
this.properties = properties;
this.version = version;
}

public Properties getProperties() {
return properties;
}

public Long getVersion() {
return version;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package com.netflix.exhibitor.core.config.consul;

import com.google.common.net.HostAndPort;
import com.netflix.exhibitor.core.config.LoadedInstanceConfig;
import com.netflix.exhibitor.core.config.PropertyBasedInstanceConfig;
import com.netflix.exhibitor.core.config.StringConfigs;
import com.orbitz.consul.Consul;
import com.pszymczyk.consul.ConsulProcess;
import com.pszymczyk.consul.ConsulStarterBuilder;
import org.apache.curator.test.Timing;
import org.apache.curator.utils.CloseableUtils;
import org.testng.Assert;
import org.testng.annotations.*;

import java.util.Properties;


public class TestConsulConfigProvider {
private Timing timing;
private ConsulProcess consul;
private Consul client;

@BeforeClass
public void setupClass() throws Exception {
consul = ConsulStarterBuilder.consulStarter().build().start();
}

@AfterClass
public void tearDownClass()
{
consul.close();
}

@BeforeMethod
public void setup() throws Exception {
timing = new Timing();

consul.reset();
client = Consul.builder()
.withHostAndPort(HostAndPort.fromParts("localhost", consul.getHttpPort()))
.build();
}

@Test
public void testBasic() throws Exception {
ConsulConfigProvider config = new ConsulConfigProvider(client, "prefix", new Properties());

try {
config.start();
config.loadConfig();

Properties properties = new Properties();
properties.setProperty(PropertyBasedInstanceConfig.toName(StringConfigs.ZOO_CFG_EXTRA, PropertyBasedInstanceConfig.ROOT_PROPERTY_PREFIX), "1,2,3");
config.storeConfig(new PropertyBasedInstanceConfig(properties, new Properties()), 0);

timing.sleepABit();

LoadedInstanceConfig instanceConfig = config.loadConfig();
Assert.assertEquals(instanceConfig.getConfig().getRootConfig().getString(StringConfigs.ZOO_CFG_EXTRA), "1,2,3");
}
finally {
CloseableUtils.closeQuietly(config);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,14 @@ private OptionSection(String sectionName, Options options)
public static final String ZOOKEEPER_CONFIG_POLLING = "zkconfigpollms";
public static final String NONE_CONFIG_DIRECTORY = "noneconfigdir";
public static final String INITIAL_CONFIG_FILE = "defaultconfig";
public static final String CONSUL_CONFIG_HOST = "consulhost";
public static final String CONSUL_CONFIG_PORT = "consulport";
public static final String CONSUL_CONFIG_KEY_PREFIX = "consulprefix";
public static final String CONSUL_CONFIG_ACL_TOKEN = "consulacltoken";
public static final String CONSUL_CONFIG_SSL = "consulssl";
public static final String CONSUL_CONFIG_SSL_VERIFY_HOSTNAME = "consulsslverifyhostname";
public static final String CONSUL_CONFIG_SSL_PROTOCOL = "consulsslprotocol";
public static final String CONSUL_CONFIG_SSL_CA_CERT = "consulsslcacert";

public static final String FILESYSTEMBACKUP = "filesystembackup";
public static final String TIMEOUT = "timeout";
Expand Down Expand Up @@ -158,6 +166,16 @@ public ExhibitorCLI()
s3Options.addOption(null, S3_REGION, true, "Optional region for S3 calls (e.g. \"eu-west-1\"). Will be used to set the S3 client's endpoint.");
s3Options.addOption(null, S3_PROXY, true, "Optional configuration used when when connecting to S3 via a proxy. Argument is the path to an AWS credential properties file with four properties (only host, port and protocol are required if using a proxy): " + PropertyBasedS3ClientConfig.PROPERTY_S3_PROXY_HOST + ", " + PropertyBasedS3ClientConfig.PROPERTY_S3_PROXY_PORT + ", " + PropertyBasedS3ClientConfig.PROPERTY_S3_PROXY_USERNAME + ", " + PropertyBasedS3ClientConfig.PROPERTY_S3_PROXY_PASSWORD);

Options consulConfigOptions = new Options();
consulConfigOptions.addOption(null, CONSUL_CONFIG_HOST, true, "Consul host; defaults to \"localhost\"");
consulConfigOptions.addOption(null, CONSUL_CONFIG_PORT, true, "Consul HTTP(s) port; defaults to 8500");
consulConfigOptions.addOption(null, CONSUL_CONFIG_ACL_TOKEN, true, "Optional Consul ACL token");
consulConfigOptions.addOption(null, CONSUL_CONFIG_SSL, true, "If true, enables Consul communication over SSL");
consulConfigOptions.addOption(null, CONSUL_CONFIG_SSL_VERIFY_HOSTNAME, true, "If true, verify SSL hostnames");
consulConfigOptions.addOption(null, CONSUL_CONFIG_SSL_PROTOCOL, true, "Consul SSL/TLS protocol; defaults to \"TLSv1.2\"");
consulConfigOptions.addOption(null, CONSUL_CONFIG_SSL_CA_CERT, true, "Path to the consul CA cert file");
consulConfigOptions.addOption(null, CONSUL_CONFIG_KEY_PREFIX, true, "Prefix in the key-value store under which to store Exhibitor data, e.g. \"exhibitor/\"");

generalOptions = new Options();
generalOptions.addOption(null, TIMEOUT, true, "Connection timeout (ms) for ZK connections. Default is 30000.");
generalOptions.addOption(null, LOGLINES, true, "Max lines of logging to keep in memory for display. Default is 1000.");
Expand All @@ -167,7 +185,7 @@ public ExhibitorCLI()
generalOptions.addOption(null, NODE_MUTATIONS, true, "If true, the Explorer UI will allow nodes to be modified (use with caution). Default is true.");
generalOptions.addOption(null, JQUERY_STYLE, true, "Styling used for the JQuery-based UI. Currently available options: " + getStyleOptions());
generalOptions.addOption(ALT_HELP, HELP, false, "Print this help");
generalOptions.addOption(SHORT_CONFIG_TYPE, CONFIG_TYPE, true, "Defines which configuration type you want to use. Choices are: \"file\", \"s3\", \"zookeeper\" or \"none\". Additional config will be required depending on which type you are using.");
generalOptions.addOption(SHORT_CONFIG_TYPE, CONFIG_TYPE, true, "Defines which configuration type you want to use. Choices are: \"file\", \"s3\", \"zookeeper\", \"consul\" or \"none\". Additional config will be required depending on which type you are using.");
generalOptions.addOption(null, CONFIGCHECKMS, true, "Period (ms) to check for shared config updates. Default is: 30000");
generalOptions.addOption(null, SERVO_INTEGRATION, true, "true/false (default is false). If enabled, ZooKeeper will be queried once a minute for its state via the 'mntr' four letter word (this requires ZooKeeper 3.4.x+). Servo will be used to publish this data via JMX.");
generalOptions.addOption(null, INITIAL_CONFIG_FILE, true, "Full path to a file that contains initial/default values for Exhibitor/ZooKeeper config values. The file is a standard property file. The property names are listed below. The file can specify some or all of the properties.");
Expand All @@ -183,6 +201,7 @@ public ExhibitorCLI()
addAll("Configuration Options for Type \"s3\"", s3ConfigOptions);
addAll("Configuration Options for Type \"zookeeper\"", zookeeperConfigOptions);
addAll("Configuration Options for Type \"file\"", fileConfigOptions);
addAll("Configuration Options for Type \"consul\"", consulConfigOptions);
addAll("Configuration Options for Type \"none\"", noneConfigOptions);
addAll("Backup Options", backupOptions);
addAll("Authorization Options", authOptions);
Expand Down
Loading

0 comments on commit d746911

Please sign in to comment.