initial sidecar impl.

use netflix-ribbon configuration
This commit is contained in:
Spencer Gibb
2015-01-26 23:30:27 -07:00
parent 2573a0b1e9
commit 5eb310362a
23 changed files with 424 additions and 34 deletions

View File

@@ -12,16 +12,3 @@ Preview of Spring Cloud Consul implementation
6. visit [http://localhost:8080](http://localhost:8080), verify that `{"serviceId":"<yourhost>:8080","host":"<yourhost>","port":8080}` results
5. run `java -jar spring-cloud-consul-sample/target/spring-cloud-consul-sample-1.0.0.BUILD-SNAPSHOT.jar --server.port=8081`
6. visit [http://localhost:8080](http://localhost:8080) again, verify that `{"serviceId":"<yourhost>:8081","host":"<yourhost>","port":8081}` eventually shows up in the results in a round robbin fashion (may take a minute or so).
### TODO
- [X] consul config
- [X] consul service discovery
- [X] consul ribbon load balancer
- [X] consul ui (on the sample agent)
- [X] consul property source
- [X] consul event bus
- [X] send messages
- [X] receive messages
- [ ] consul locks
- [ ] consul leader election

35
pom.xml
View File

@@ -30,6 +30,7 @@
<module>spring-cloud-consul-config</module>
<module>spring-cloud-consul-discovery</module>
<module>spring-cloud-consul-bus</module>
<module>spring-cloud-consul-sidecar</module>
<module>spring-cloud-consul-sample</module>
</modules>
@@ -106,6 +107,26 @@
<artifactId>spring-cloud-config-client</artifactId>
<version>1.0.0.BUILD-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-netflix-sidecar</artifactId>
<version>1.0.0.BUILD-SNAPSHOT</version>
<exclusions>
<exclusion>
<groupId>com.netflix.eureka</groupId>
<artifactId>eureka-client</artifactId>
</exclusion>
<exclusion>
<groupId>com.netflix.ribbon</groupId>
<artifactId>ribbon-eureka</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-netflix-core</artifactId>
<version>1.0.0.BUILD-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.netflix.feign</groupId>
<artifactId>feign-core</artifactId>
@@ -116,6 +137,11 @@
<artifactId>feign-jackson</artifactId>
<version>${feign.version}</version>
</dependency>
<dependency>
<groupId>com.netflix.feign</groupId>
<artifactId>feign-slf4j</artifactId>
<version>${feign.version}</version>
</dependency>
<dependency>
<groupId>com.netflix.ribbon</groupId>
<artifactId>ribbon</artifactId>
@@ -126,6 +152,11 @@
<artifactId>ribbon-core</artifactId>
<version>${ribbon.version}</version>
</dependency>
<dependency>
<groupId>com.netflix.ribbon</groupId>
<artifactId>ribbon-httpclient</artifactId>
<version>${ribbon.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
@@ -141,8 +172,8 @@
</dependencyManagement>
<properties>
<feign.version>6.1.2</feign.version>
<ribbon.version>2.0-RC9</ribbon.version>
<feign.version>7.1.0</feign.version>
<ribbon.version>2.0-RC13</ribbon.version>
</properties>
</project>

View File

@@ -36,6 +36,10 @@
<groupId>com.netflix.feign</groupId>
<artifactId>feign-jackson</artifactId>
</dependency>
<dependency>
<groupId>com.netflix.feign</groupId>
<artifactId>feign-slf4j</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>

View File

@@ -1,9 +1,9 @@
package org.springframework.cloud.consul.client;
import feign.Param;
import feign.RequestLine;
import org.springframework.cloud.consul.model.Service;
import javax.inject.Named;
import java.util.Map;
/**
@@ -21,5 +21,5 @@ public interface AgentClient {
void register(Service service);
@RequestLine("PUT /v1/agent/service/deregister/{serviceId}")
void deregister(@Named("serviceId") String serviceId);
void deregister(@Param("serviceId") String serviceId);
}

View File

@@ -1,9 +1,9 @@
package org.springframework.cloud.consul.client;
import feign.Param;
import feign.RequestLine;
import org.springframework.cloud.consul.model.ServiceNode;
import javax.inject.Named;
import java.util.List;
import java.util.Map;
@@ -15,5 +15,5 @@ public interface CatalogClient {
Map<String, List<String>> getServices();
@RequestLine("GET /v1/catalog/service/{serviceId}")
List<ServiceNode> getServiceNodes(@Named("serviceId") String serviceId);
List<ServiceNode> getServiceNodes(@Param("serviceId") String serviceId);
}

View File

@@ -1,10 +1,10 @@
package org.springframework.cloud.consul.client;
import feign.Param;
import feign.RequestLine;
import feign.Response;
import org.springframework.cloud.consul.model.Event;
import javax.inject.Named;
import java.util.List;
/**
@@ -13,7 +13,7 @@ import java.util.List;
public interface EventClient {
//?node=, ?service=, and ?tag= ?dc=
@RequestLine("PUT /v1/event/fire/{name}")
Event fire(@Named("name") String name, String payload);
Event fire(@Param("name") String name, String payload);
//?name=
//?wait=<interval>&index=<idx>
@@ -24,5 +24,5 @@ public interface EventClient {
Response getEventsResponse();
@RequestLine("GET /v1/event/list?wait={wait}&index={index}")
Response watch(@Named("wait") String wait, @Named("index") String index);
Response watch(@Param("wait") String wait, @Param("index") String index);
}

View File

@@ -3,12 +3,12 @@ package org.springframework.cloud.consul.client;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.type.TypeFactory;
import com.google.common.base.Throwables;
import feign.Param;
import feign.Response;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.consul.model.Event;
import javax.annotation.PostConstruct;
import javax.inject.Named;
import java.io.IOException;
import java.math.BigInteger;
import java.util.ArrayList;
@@ -46,7 +46,7 @@ public class EventService {
return lastIndex.get();
}
public Event fire(@Named("name") String name, String payload) {
public Event fire(@Param("name") String name, String payload) {
return client.fire(name, payload);
}

View File

@@ -1,9 +1,9 @@
package org.springframework.cloud.consul.client;
import feign.Param;
import feign.RequestLine;
import org.springframework.cloud.consul.model.KeyValue;
import javax.inject.Named;
import java.util.List;
/**
@@ -11,23 +11,23 @@ import java.util.List;
*/
public interface KeyValueClient {
@RequestLine("GET /v1/kv/{key}")
List<KeyValue> getKeyValue(@Named("key") String key);
List<KeyValue> getKeyValue(@Param("key") String key);
@RequestLine("GET /v1/kv/?recurse=true")
List<KeyValue> getKeyValueRecurse();
@RequestLine("GET /v1/kv/{key}?recurse=true")
List<KeyValue> getKeyValueRecurse(@Named("key") String key);
List<KeyValue> getKeyValueRecurse(@Param("key") String key);
@RequestLine("GET /v1/kv/?keys=true")
List<String> getKeys();
@RequestLine("GET /v1/kv/{key}?keys=true")
List<String> getKeys(@Named("key") String key);
List<String> getKeys(@Param("key") String key);
@RequestLine("PUT /v1/kv/{key}")
boolean put(@Named("key") String key, Object value);
boolean put(@Param("key") String key, Object value);
@RequestLine("DELETE /v1/kv/{key}")
void delete(@Named("key") String key);
void delete(@Param("key") String key);
}

View File

@@ -20,6 +20,10 @@
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-consul-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-netflix-core</artifactId>
</dependency>
<dependency>
<groupId>com.netflix.ribbon</groupId>
<artifactId>ribbon</artifactId>
@@ -28,6 +32,10 @@
<groupId>com.netflix.ribbon</groupId>
<artifactId>ribbon-core</artifactId>
</dependency>
<dependency>
<groupId>com.netflix.ribbon</groupId>
<artifactId>ribbon-httpclient</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>

View File

@@ -13,10 +13,10 @@ public class ConsulDiscoveryClientConfiguration {
return new ConsulLifecycle();
}
@Bean
/*@Bean
public ConsulLoadBalancerClient consulLoadBalancerClient() {
return new ConsulLoadBalancerClient();
}
}*/
@Bean
public ConsulDiscoveryClient consulDiscoveryClient() {

View File

@@ -0,0 +1,122 @@
/*
* Copyright 2013-2015 the original author or authors.
*
* 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 org.springframework.cloud.consul.discovery;
import static com.netflix.client.config.CommonClientConfigKey.*;
import javax.annotation.PostConstruct;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.cloud.consul.client.CatalogClient;
import org.springframework.cloud.netflix.ribbon.ZonePreferenceServerListFilter;
import org.springframework.context.annotation.Configuration;
import com.netflix.config.ConfigurationManager;
import com.netflix.config.DynamicPropertyFactory;
import com.netflix.config.DynamicStringProperty;
import com.netflix.loadbalancer.DynamicServerListLoadBalancer;
import com.netflix.loadbalancer.ServerList;
import com.netflix.loadbalancer.ZoneAvoidanceRule;
/**
* Preprocessor that configures defaults for eureka-discovered ribbon clients. Such as:
* <code>@zone</code>, NIWSServerListClassName, DeploymentContextBasedVipAddresses,
* NFLoadBalancerRuleClassName, NIWSServerListFilterClassName and more
*
* @author Spencer Gibb
* @author Dave Syer
*/
@Configuration
public class ConsulRibbonClientConfiguration implements BeanPostProcessor {
@Autowired
CatalogClient client;
@Value("${ribbon.client.name}")
private String serviceId = "client";
protected static final String VALUE_NOT_SET = "__not__set__";
protected static final String DEFAULT_NAMESPACE = "ribbon";
public ConsulRibbonClientConfiguration() {
System.out.println("here");
}
public ConsulRibbonClientConfiguration(String serviceId) {
this.serviceId = serviceId;
}
@PostConstruct
public void preprocess() {
// TODO: should this look more like hibernate spring boot props?
setProp(this.serviceId, NIWSServerListClassName.key(),
ConsulServerList.class.getName());
// FIXME: what should this be?
setProp(this.serviceId, DeploymentContextBasedVipAddresses.key(), this.serviceId);
setProp(this.serviceId, NFLoadBalancerRuleClassName.key(),
ZoneAvoidanceRule.class.getName());
setProp(this.serviceId, NIWSServerListFilterClassName.key(),
ZonePreferenceServerListFilter.class.getName());
setProp(this.serviceId, EnableZoneAffinity.key(), "true");
}
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName)
throws BeansException {
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName)
throws BeansException {
if (bean instanceof DynamicServerListLoadBalancer) {
wrapServerList((DynamicServerListLoadBalancer<?>) bean);
}
return bean;
}
private void wrapServerList(DynamicServerListLoadBalancer<?> balancer) {
@SuppressWarnings("unchecked")
DynamicServerListLoadBalancer<ConsulServer> dynamic = (DynamicServerListLoadBalancer<ConsulServer>) balancer;
ServerList<ConsulServer> list = dynamic.getServerListImpl();
if (list instanceof ConsulServerList) {
ConsulServerList csl = (ConsulServerList) list;
csl.setClient(client);
}
}
protected void setProp(String serviceId, String suffix, String value) {
// how to set the namespace properly?
String key = getKey(serviceId, suffix);
DynamicStringProperty property = getProperty(key);
if (property.get().equals(VALUE_NOT_SET)) {
ConfigurationManager.getConfigInstance().setProperty(key, value);
}
}
protected DynamicStringProperty getProperty(String key) {
return DynamicPropertyFactory.getInstance().getStringProperty(key, VALUE_NOT_SET);
}
protected String getKey(String serviceId, String suffix) {
return serviceId + "." + DEFAULT_NAMESPACE + "." + suffix;
}
}

View File

@@ -23,14 +23,21 @@ public class ConsulServerList extends AbstractServerList<ConsulServer> {
private String serviceId;
public ConsulServerList() {
}
public ConsulServerList(CatalogClient client, String serviceId) {
this.client = client;
this.serviceId = serviceId;
}
public void setClient(CatalogClient client) {
this.client = client;
}
@Override
public void initWithNiwsConfig(IClientConfig clientConfig) {
this.serviceId = clientConfig.getClientName();
}
@Override
@@ -44,6 +51,9 @@ public class ConsulServerList extends AbstractServerList<ConsulServer> {
}
private List<ConsulServer> getServers() {
if (client == null) {
return Collections.emptyList();
}
List<ServiceNode> nodes = client.getServiceNodes(serviceId);
if (nodes == null || nodes.isEmpty()) {
return Collections.EMPTY_LIST;

View File

@@ -0,0 +1,39 @@
/*
* Copyright 2013-2014 the original author or authors.
*
* 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 org.springframework.cloud.consul.discovery;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.netflix.ribbon.RibbonAutoConfiguration;
import org.springframework.cloud.netflix.ribbon.RibbonClients;
import org.springframework.cloud.netflix.ribbon.SpringClientFactory;
import org.springframework.context.annotation.Configuration;
/**
* @author Dave Syer
*/
@Configuration
@EnableConfigurationProperties
@ConditionalOnBean(SpringClientFactory.class)
@ConditionalOnProperty(value = "ribbon.consul.enabled", matchIfMissing = true)
@AutoConfigureAfter(RibbonAutoConfiguration.class)
@RibbonClients(defaultConfiguration = ConsulRibbonClientConfiguration.class)
public class RibbonConsulAutoConfiguration {
}

View File

@@ -1,3 +1,6 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.consul.discovery.RibbonConsulAutoConfiguration
# Discovery Client Configuration
org.springframework.cloud.client.discovery.EnableDiscoveryClient=\
org.springframework.cloud.consul.discovery.ConsulDiscoveryClientConfiguration

View File

@@ -7,9 +7,10 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.bind.RelaxedPropertyResolver;
import org.springframework.cloud.bus.jackson.SubtypeModule;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.cloud.consul.bus.SimpleRemoteEvent;
import org.springframework.cloud.consul.discovery.ConsulLoadBalancerClient;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -31,7 +32,10 @@ public class SampleApplication implements ApplicationListener<SimpleRemoteEvent>
public static final String CLIENT_NAME = "testConsulApp";
@Autowired
ConsulLoadBalancerClient loadBalancer;
LoadBalancerClient loadBalancer;
@Autowired
DiscoveryClient discoveryClient;
@Autowired
Environment env;
@@ -39,6 +43,11 @@ public class SampleApplication implements ApplicationListener<SimpleRemoteEvent>
@Autowired(required = false)
RelaxedPropertyResolver resolver;
@RequestMapping("/me")
public ServiceInstance me() {
return discoveryClient.getLocalServiceInstance();
}
@RequestMapping("/")
public ServiceInstance lb() {
return loadBalancer.choose(CLIENT_NAME);

View File

@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-cloud-consul-sidecar</artifactId>
<packaging>jar</packaging>
<name>Spring Cloud Consul Sidecar</name>
<description>Spring Cloud Consul Sidecar</description>
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-consul</artifactId>
<version>1.0.0.BUILD-SNAPSHOT</version>
<relativePath>..</relativePath>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-consul-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-consul-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-netflix-sidecar</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<!-- Only needed at compile time -->
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1 @@
python -m SimpleHTTPServer

View File

@@ -0,0 +1,10 @@
package org.springframework.cloud.consul.sidecar;
import org.springframework.context.annotation.Configuration;
/**
* @author Spencer Gibb
*/
@Configuration
public class ConsulSidecarConfiguration {
}

View File

@@ -0,0 +1,23 @@
package org.springframework.cloud.consul.sidecar;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.sidecar.SidecarConfiguration;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.context.annotation.Import;
import java.lang.annotation.*;
/**
* @author Spencer Gibb
*/
@EnableCircuitBreaker
@EnableDiscoveryClient
@EnableZuulProxy
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({SidecarConfiguration.class, ConsulSidecarConfiguration.class})
public @interface EnableSidecar {
}

View File

@@ -0,0 +1,56 @@
/*
* Copyright 2013-2015 the original author or authors.
*
* 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 org.springframework.cloud.consul.sidecar;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.actuate.metrics.repository.InMemoryMetricRepository;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.IntegrationTest;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.cloud.netflix.sidecar.*;
import org.springframework.context.annotation.Bean;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.web.bind.annotation.RestController;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = SidecarApplicationTests.SidecarApplication.class)
@IntegrationTest("server.port=0")
public class SidecarApplicationTests {
@Test
public void contextLoads() {
}
@SpringBootApplication
@org.springframework.cloud.netflix.sidecar.EnableSidecar
@RestController
public static class SidecarApplication {
@Bean
public InMemoryMetricRepository inMemoryMetricRepository() {
return new InMemoryMetricRepository();
}
public static void main(String[] args) {
SpringApplication.run(SidecarApplication.class, args);
}
}
}

View File

@@ -0,0 +1,21 @@
server:
port: 5678
spring:
application:
name: sidecarTest
sidecar:
port: 8000
health-uri: http://localhost:8000/src/test/resources/health.json
ribbon:
ServerListRefreshInterval: 5000
endpoints:
refresh:
enabled: true
shutdown:
enabled: true
health:
sensitive: false

View File

@@ -0,0 +1,7 @@
spring:
#application:
# name: sideCarTest
cloud:
config:
username: user
password: password

View File

@@ -0,0 +1 @@
{"status":"UP"}