Commit 8a6cdfa9 by Johannes Edmeier

Update to Spring Cloud Brixton

Deriving from Spring Clouds ProxyRouteLocator has become too difficult. It would have required to either set fixed ZuulProperties or providing a DiscoveryClient. So we are shipping our own lean version of it. Since the PreDecorationFilter is tightly coupled to the ProxyRouteLocator we also have to use our own stripped down version. closes #123
parent 184528cf
......@@ -19,7 +19,7 @@
<main.basedir>${basedir}</main.basedir>
<passphrase>${gpg.passphrase}</passphrase>
<spring-boot.version>1.3.0.RC1</spring-boot.version>
<spring-cloud.version>Angel.SR3</spring-cloud.version>
<spring-cloud.version>Brixton.M2</spring-cloud.version>
<build-plugin.jacoco.version>0.7.5.201505241946</build-plugin.jacoco.version>
<build-plugin.coveralls.version>4.0.0</build-plugin.coveralls.version>
<build-plugin.gpg.version>1.6</build-plugin.gpg.version>
......@@ -47,6 +47,10 @@
</scm>
<developers>
<developer>
<name>Johannes Edmeier</name>
<email>johannes.edmeier@gmail.com</email>
</developer>
<developer>
<name>Thomas Bosch</name>
<email>thomas.bosch@codecentric.de</email>
<organization>codecentric AG</organization>
......
......@@ -11,8 +11,6 @@ spring:
eureka:
instance:
leaseRenewalIntervalInSeconds: 5
metadataMap:
instanceId: ${spring.application.name}:${spring.application.instance_id:${random.value}}
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
\ No newline at end of file
......@@ -25,11 +25,6 @@
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!-- Use Zuul WITHOUT Hystrix/Ribbon/Config Client -->
<dependency>
<groupId>com.netflix.zuul</groupId>
......@@ -58,13 +53,12 @@
<groupId>com.hazelcast</groupId>
<artifactId>hazelcast</artifactId>
<optional>true</optional>
<!-- https://github.com/spring-projects/spring-boot/issues/3418 -->
<exclusions>
<exclusion>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Optional Configuration Processor for metadata -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!-- Test -->
<dependency>
......
......@@ -18,21 +18,14 @@ package de.codecentric.boot.admin.config;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.actuate.endpoint.Endpoint;
import org.springframework.boot.actuate.trace.TraceRepository;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.netflix.zuul.RoutesEndpoint;
import org.springframework.cloud.netflix.zuul.ZuulFilterInitializer;
import org.springframework.cloud.netflix.zuul.filters.ProxyRequestHelper;
import org.springframework.cloud.netflix.zuul.filters.ProxyRouteLocator;
import org.springframework.cloud.netflix.zuul.filters.ZuulProperties;
import org.springframework.cloud.netflix.zuul.filters.post.SendErrorFilter;
import org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter;
import org.springframework.cloud.netflix.zuul.filters.pre.DebugFilter;
import org.springframework.cloud.netflix.zuul.filters.pre.FormBodyWrapperFilter;
import org.springframework.cloud.netflix.zuul.filters.pre.PreDecorationFilter;
import org.springframework.cloud.netflix.zuul.filters.pre.Servlet30WrapperFilter;
import org.springframework.cloud.netflix.zuul.filters.route.SimpleHostRoutingFilter;
import org.springframework.cloud.netflix.zuul.web.ZuulController;
......@@ -47,18 +40,15 @@ import de.codecentric.boot.admin.controller.RegistryController;
import de.codecentric.boot.admin.event.RoutesOutdatedEvent;
import de.codecentric.boot.admin.registry.ApplicationRegistry;
import de.codecentric.boot.admin.zuul.ApplicationRouteLocator;
import de.codecentric.boot.admin.zuul.PreDecorationFilter;
@Configuration
@EnableConfigurationProperties(ZuulProperties.class)
public class RevereseZuulProxyConfiguration {
@Autowired(required = false)
private TraceRepository traces;
@Autowired
private ZuulProperties zuulProperties;
@Autowired
private ServerProperties server;
@Autowired
......@@ -67,12 +57,12 @@ public class RevereseZuulProxyConfiguration {
@Bean
public ApplicationRouteLocator routeLocator() {
return new ApplicationRouteLocator(this.server.getServletPrefix(), registry,
this.zuulProperties, RegistryController.PATH);
RegistryController.PATH);
}
@Bean
public PreDecorationFilter preDecorationFilter() {
return new PreDecorationFilter(routeLocator(), this.zuulProperties.isAddProxyHeaders());
return new PreDecorationFilter(routeLocator(), true);
}
@Bean
......@@ -125,7 +115,6 @@ public class RevereseZuulProxyConfiguration {
@Configuration
protected static class ZuulFilterConfiguration {
@Autowired
private Map<String, ZuulFilter> filters;
......@@ -133,7 +122,6 @@ public class RevereseZuulProxyConfiguration {
public ZuulFilterInitializer zuulFilterInitializer() {
return new ZuulFilterInitializer(this.filters);
}
}
@EventListener
......@@ -142,18 +130,4 @@ public class RevereseZuulProxyConfiguration {
zuulHandlerMapping().registerHandlers();
}
@Configuration
@ConditionalOnClass(Endpoint.class)
protected static class RoutesEndpointConfiguration {
@Autowired
private ProxyRouteLocator routeLocator;
@Bean
public RoutesEndpoint zuulEndpoint() {
return new RoutesEndpoint(this.routeLocator);
}
}
}
......@@ -69,7 +69,7 @@ public class RegistryController {
@RequestMapping(method = RequestMethod.GET)
public Collection<Application> applications(
@RequestParam(value = "name", required = false) String name) {
LOGGER.debug("Deliver registered applications with name= {}", name);
LOGGER.debug("Deliver registered applications with name={}", name);
if (name == null || name.isEmpty()) {
return registry.getApplications();
} else {
......
......@@ -15,11 +15,19 @@
*/
package de.codecentric.boot.admin.zuul;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.atomic.AtomicReference;
import org.springframework.cloud.netflix.zuul.filters.ProxyRouteLocator;
import org.springframework.cloud.netflix.zuul.filters.ZuulProperties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.netflix.zuul.filters.RouteLocator;
import org.springframework.cloud.netflix.zuul.filters.ZuulProperties.ZuulRoute;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;
import org.springframework.util.StringUtils;
import de.codecentric.boot.admin.model.Application;
......@@ -30,31 +38,29 @@ import de.codecentric.boot.admin.registry.ApplicationRegistry;
*
* @author Johannes Stelzer
*/
public class ApplicationRouteLocator extends ProxyRouteLocator {
public class ApplicationRouteLocator implements RouteLocator {
private static final Logger LOGGER = LoggerFactory.getLogger(ApplicationRouteLocator.class);
private AtomicReference<Map<String, ZuulRoute>> routes = new AtomicReference<>();
private ApplicationRegistry registry;
private String prefix;
private PathMatcher pathMatcher = new AntPathMatcher();
private String servletPath;
public ApplicationRouteLocator(String servletPath, ApplicationRegistry registry,
ZuulProperties properties, String prefix) {
super(servletPath, null, properties);
String prefix) {
this.servletPath = servletPath;
this.registry = registry;
this.prefix = prefix;
}
@Override
protected LinkedHashMap<String, ZuulRoute> locateRoutes() {
LinkedHashMap<String, ZuulRoute> locateRoutes = super.locateRoutes();
if (registry != null) {
for (Application application : registry.getApplications()) {
addRoute(locateRoutes, prefix + "/" + application.getId() + "/health/**",
application.getHealthUrl());
if (!StringUtils.isEmpty(application.getManagementUrl())) {
addRoute(locateRoutes, prefix + "/" + application.getId() + "/*/**",
application.getManagementUrl());
}
private LinkedHashMap<String, ZuulRoute> locateRoutes() {
LinkedHashMap<String, ZuulRoute> locateRoutes = new LinkedHashMap<String, ZuulRoute>();
for (Application application : registry.getApplications()) {
addRoute(locateRoutes, prefix + "/" + application.getId() + "/health/**",
application.getHealthUrl());
if (!StringUtils.isEmpty(application.getManagementUrl())) {
addRoute(locateRoutes, prefix + "/" + application.getId() + "/*/**",
application.getManagementUrl());
}
}
......@@ -65,4 +71,80 @@ public class ApplicationRouteLocator extends ProxyRouteLocator {
locateRoutes.put(path, new ZuulRoute(path, url));
}
public ProxyRouteSpec getMatchingRoute(String path) {
LOGGER.info("Finding route for path: {}; servletPath={}", path, this.servletPath);
if (StringUtils.hasText(this.servletPath) && !this.servletPath.equals("/")
&& path.startsWith(this.servletPath)) {
path = path.substring(this.servletPath.length());
}
for (Entry<String, ZuulRoute> entry : this.routes.get().entrySet()) {
String pattern = entry.getKey();
LOGGER.debug("Matching pattern: {}", pattern);
if (this.pathMatcher.match(pattern, path)) {
ZuulRoute route = entry.getValue();
int index = route.getPath().indexOf("*") - 1;
String routePrefix = route.getPath().substring(0, index);
String targetPath = path.replaceFirst(routePrefix, "");
return new ProxyRouteSpec(route.getId(), targetPath, route.getLocation(),
routePrefix);
}
}
return null;
}
@Override
public Collection<String> getRoutePaths() {
return getRoutes().keySet();
}
public void resetRoutes() {
this.routes.set(locateRoutes());
}
public Map<String, String> getRoutes() {
if (this.routes.get() == null) {
this.routes.set(locateRoutes());
}
Map<String, String> values = new LinkedHashMap<>();
for (String key : this.routes.get().keySet()) {
String url = key;
values.put(url, this.routes.get().get(key).getLocation());
}
return values;
}
@Override
public Collection<String> getIgnoredPaths() {
return Collections.emptyList();
}
public static class ProxyRouteSpec {
private final String id;
private final String path;
private final String location;
private final String prefix;
public ProxyRouteSpec(String id, String path, String location, String prefix) {
this.id = id;
this.path = path;
this.location = location;
this.prefix = prefix;
}
public String getId() {
return id;
}
public String getPath() {
return path;
}
public String getLocation() {
return location;
}
public String getPrefix() {
return prefix;
}
}
}
/*
* Copyright 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 de.codecentric.boot.admin.zuul;
import java.net.MalformedURLException;
import java.net.URL;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;
import org.springframework.web.util.UrlPathHelper;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.constants.ZuulHeaders;
import com.netflix.zuul.context.RequestContext;
import de.codecentric.boot.admin.zuul.ApplicationRouteLocator.ProxyRouteSpec;
/**
* @author Johannes Edmeier
*/
public class PreDecorationFilter extends ZuulFilter {
private static final Logger LOGGER = LoggerFactory.getLogger(PreDecorationFilter.class);
private ApplicationRouteLocator routeLocator;
private boolean addProxyHeaders;
private UrlPathHelper urlPathHelper = new UrlPathHelper();
public PreDecorationFilter(ApplicationRouteLocator routeLocator, boolean addProxyHeaders) {
this.routeLocator = routeLocator;
this.addProxyHeaders = addProxyHeaders;
}
@Override
public int filterOrder() {
return 5;
}
@Override
public String filterType() {
return "pre";
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
final String requestURI = this.urlPathHelper.getPathWithinApplication(ctx.getRequest());
ProxyRouteSpec route = this.routeLocator.getMatchingRoute(requestURI);
if (route != null) {
ctx.put("requestURI", route.getPath());
ctx.put("proxy", route.getId());
ctx.setRouteHost(getUrl(route.getLocation()));
ctx.addOriginResponseHeader("X-Zuul-Service", route.getLocation());
if (this.addProxyHeaders) {
ctx.addZuulRequestHeader("X-Forwarded-Host", ctx.getRequest().getServerName() + ":"
+ String.valueOf(ctx.getRequest().getServerPort()));
ctx.addZuulRequestHeader(ZuulHeaders.X_FORWARDED_PROTO,
ctx.getRequest().getScheme());
if (StringUtils.hasText(route.getPrefix())) {
ctx.addZuulRequestHeader("X-Forwarded-Prefix", route.getPrefix());
}
}
} else {
LOGGER.warn("No route found for uri: " + requestURI);
ctx.set("forward.to", requestURI);
}
return null;
}
private URL getUrl(String target) {
try {
return new URL(target);
} catch (MalformedURLException ex) {
throw new IllegalStateException("Target URL is malformed", ex);
}
}
}
......@@ -52,8 +52,8 @@ public class AdminApplicationTest {
@Test
public void testGetApplications() {
@SuppressWarnings("rawtypes")
ResponseEntity<List> entity = new TestRestTemplate().getForEntity("http://localhost:"
+ port + "/api/applications", List.class);
ResponseEntity<List> entity = new TestRestTemplate()
.getForEntity("http://localhost:" + port + "/api/applications", List.class);
assertEquals(HttpStatus.OK, entity.getStatusCode());
}
......@@ -68,13 +68,19 @@ public class AdminApplicationTest {
application, Application.class);
@SuppressWarnings("rawtypes")
ResponseEntity<Map> info = new TestRestTemplate().getForEntity(apiBaseUrl + "/"
+ entity.getBody().getId() + "/info", Map.class);
ResponseEntity<Map> app = new TestRestTemplate()
.getForEntity(apiBaseUrl + "/" + entity.getBody().getId(), Map.class);
assertEquals(HttpStatus.OK, app.getStatusCode());
assertEquals("TestApp", app.getBody().get("name"));
@SuppressWarnings("rawtypes")
ResponseEntity<Map> info = new TestRestTemplate()
.getForEntity(apiBaseUrl + "/" + entity.getBody().getId() + "/info", Map.class);
assertEquals(HttpStatus.OK, info.getStatusCode());
@SuppressWarnings("rawtypes")
ResponseEntity<Map> health = new TestRestTemplate().getForEntity(apiBaseUrl + "/"
+ entity.getBody().getId() + "/health", Map.class);
ResponseEntity<Map> health = new TestRestTemplate()
.getForEntity(apiBaseUrl + "/" + entity.getBody().getId() + "/health", Map.class);
assertEquals(HttpStatus.OK, health.getStatusCode());
}
......
......@@ -15,21 +15,20 @@
*/
package de.codecentric.boot.admin.zuul;
import static java.util.Collections.singleton;
import static java.util.Collections.singletonList;
import static org.hamcrest.CoreMatchers.hasItems;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.util.Collections;
import org.junit.Before;
import org.junit.Test;
import org.springframework.cloud.netflix.zuul.filters.ProxyRouteLocator;
import org.springframework.cloud.netflix.zuul.filters.ZuulProperties;
import de.codecentric.boot.admin.model.Application;
import de.codecentric.boot.admin.registry.ApplicationRegistry;
import de.codecentric.boot.admin.zuul.ApplicationRouteLocator.ProxyRouteSpec;
public class ApplicationRouteLocatorTest {
......@@ -39,34 +38,32 @@ public class ApplicationRouteLocatorTest {
@Before
public void setup() {
registry = mock(ApplicationRegistry.class);
locator = new ApplicationRouteLocator("/", registry, new ZuulProperties(),
"/api/applications");
locator = new ApplicationRouteLocator("/", registry, "/api/applications");
}
@Test
public void locateRoutes_healthOnly() {
when(registry.getApplications()).thenReturn(
Collections.singletonList(Application.create("app1")
.withHealthUrl("http://localhost/health").withId("1234").build()));
when(registry.getApplications()).thenReturn(singletonList(Application.create("app1")
.withHealthUrl("http://localhost/health").withId("1234").build()));
locator.resetRoutes();
assertEquals(1, locator.getRoutes().size());
assertEquals(Collections.singleton("/api/applications/1234/health/**"),
locator.getRoutePaths());
assertEquals(singleton("/api/applications/1234/health/**"), locator.getRoutePaths());
assertEquals("http://localhost/health",
locator.getRoutes().get("/api/applications/1234/health/**"));
assertEquals(new ProxyRouteLocator.ProxyRouteSpec("api/applications/1234/health", "",
"http://localhost/health", "/api/applications/1234/health", null),
locator.getMatchingRoute("/api/applications/1234/health"));
ProxyRouteSpec route = locator.getMatchingRoute("/api/applications/1234/health");
assertEquals("api/applications/1234/health", route.getId());
assertEquals("", route.getPath());
assertEquals("http://localhost/health", route.getLocation());
assertEquals("/api/applications/1234/health", route.getPrefix());
}
@Test
public void locateRoutes() {
when(registry.getApplications()).thenReturn(
Collections.singletonList(Application.create("app1")
.withHealthUrl("http://localhost/health")
singletonList(Application.create("app1").withHealthUrl("http://localhost/health")
.withManagementUrl("http://localhost").withId("1234").build()));
locator.resetRoutes();
......@@ -78,14 +75,16 @@ public class ApplicationRouteLocatorTest {
assertEquals("http://localhost/health",
locator.getRoutes().get("/api/applications/1234/health/**"));
assertEquals(new ProxyRouteLocator.ProxyRouteSpec("api/applications/1234/health", "",
"http://localhost/health", "/api/applications/1234/health", null),
locator.getMatchingRoute("/api/applications/1234/health"));
assertEquals("http://localhost", locator.getRoutes().get("/api/applications/1234/*/**"));
assertEquals(new ProxyRouteLocator.ProxyRouteSpec("api/applications/1234", "/*/**",
"http://localhost", "/api/applications/1234", null),
locator.getMatchingRoute("/api/applications/1234/*/**"));
ProxyRouteSpec route = locator.getMatchingRoute("/api/applications/1234/health");
assertEquals("api/applications/1234/health", route.getId());
assertEquals("", route.getPath());
assertEquals("http://localhost/health", route.getLocation());
assertEquals("/api/applications/1234/health", route.getPrefix());
route = locator.getMatchingRoute("/api/applications/1234/notify");
assertEquals("api/applications/1234", route.getId());
assertEquals("/notify", route.getPath());
assertEquals("http://localhost", route.getLocation());
assertEquals("/api/applications/1234", route.getPrefix());
}
}
/*
* Copyright 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 de.codecentric.boot.admin.zuul;
import static org.junit.Assert.assertEquals;
import org.junit.Before;
import org.junit.Test;
import org.springframework.mock.web.MockHttpServletRequest;
import com.netflix.zuul.context.RequestContext;
import de.codecentric.boot.admin.model.Application;
import de.codecentric.boot.admin.registry.ApplicationRegistry;
import de.codecentric.boot.admin.registry.HashingApplicationUrlIdGenerator;
import de.codecentric.boot.admin.registry.store.ApplicationStore;
import de.codecentric.boot.admin.registry.store.SimpleApplicationStore;
public class PreDecorationFilterTest {
private PreDecorationFilter filter;
private ApplicationRouteLocator routeLocator;
private MockHttpServletRequest request = new MockHttpServletRequest();
private ApplicationRegistry registry;
private ApplicationStore store;
@Before
public void init() {
store = new SimpleApplicationStore();
store.save(Application.create("test").withId("-id-").withHealthUrl("http://test/health")
.withManagementUrl("http://mgmt").build());
registry = new ApplicationRegistry(store, new HashingApplicationUrlIdGenerator());
routeLocator = new ApplicationRouteLocator("/", registry, "/proxied");
routeLocator.resetRoutes();
filter = new PreDecorationFilter(routeLocator, true);
RequestContext ctx = RequestContext.getCurrentContext();
ctx.clear();
ctx.setRequest(request);
}
@Test
public void basicProperties() throws Exception {
assertEquals(5, this.filter.filterOrder());
assertEquals(true, this.filter.shouldFilter());
assertEquals("pre", this.filter.filterType());
}
@Test
public void prefixRouteAddsHeader() throws Exception {
request.setRequestURI("/proxied/-id-/foo");
filter.run();
RequestContext ctx = RequestContext.getCurrentContext();
assertEquals("/foo", ctx.get("requestURI"));
assertEquals("http://mgmt", ctx.getRouteHost().toString());
assertEquals("localhost:80", ctx.getZuulRequestHeaders().get("x-forwarded-host"));
assertEquals("http", ctx.getZuulRequestHeaders().get("x-forwarded-proto"));
assertEquals("/proxied/-id-", ctx.getZuulRequestHeaders().get("x-forwarded-prefix"));
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment