Commit 8e2ce75d by Johannes Stelzer

Merge pull request #51 from joshiste/api-gateway

Zuul-Reverse-Proxy for registered applications and removal of CORS-stuff
parents 4302e92d e52f6c75
...@@ -26,6 +26,7 @@ ...@@ -26,6 +26,7 @@
<spring-boot.version>1.2.2.RELEASE</spring-boot.version> <spring-boot.version>1.2.2.RELEASE</spring-boot.version>
<hazelcast.version>3.3.3</hazelcast.version> <hazelcast.version>3.3.3</hazelcast.version>
<commons-lang3.version>3.3.2</commons-lang3.version> <commons-lang3.version>3.3.2</commons-lang3.version>
<spring-cloud.version>1.0.0.RELEASE</spring-cloud.version>
<build-plugin.jacoco.version>0.7.3.201502191951</build-plugin.jacoco.version> <build-plugin.jacoco.version>0.7.3.201502191951</build-plugin.jacoco.version>
<build-plugin.coveralls.version>3.0.1</build-plugin.coveralls.version> <build-plugin.coveralls.version>3.0.1</build-plugin.coveralls.version>
...@@ -220,6 +221,23 @@ ...@@ -220,6 +221,23 @@
<artifactId>spring-boot-starter-web</artifactId> <artifactId>spring-boot-starter-web</artifactId>
<version>${spring-boot.version}</version> <version>${spring-boot.version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zuul</artifactId>
<version>${spring-cloud.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-netflix-core</artifactId>
<version>${spring-cloud.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-commons</artifactId>
<version>${spring-cloud.version}</version>
</dependency>
<dependency> <dependency>
<groupId>org.apache.commons</groupId> <groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId> <artifactId>commons-lang3</artifactId>
...@@ -244,4 +262,13 @@ ...@@ -244,4 +262,13 @@
</dependency> </dependency>
</dependencies> </dependencies>
</dependencyManagement> </dependencyManagement>
<repositories>
<repository>
<id>spring-release</id>
<snapshots>
<enabled>false</enabled>
</snapshots>
<url>http://repo.spring.io/release</url>
</repository>
</repositories>
</project> </project>
...@@ -32,31 +32,31 @@ module.exports = function ($resource, $http, $rootScope) { ...@@ -32,31 +32,31 @@ module.exports = function ($resource, $http, $rootScope) {
}; };
Application.prototype.getHealth = function () { Application.prototype.getHealth = function () {
return $http.get(this.url + '/health').error(new AuthInterceptor(this)); return $http.get('api/applications/' + this.id + '/health').error(new AuthInterceptor(this));
}; };
Application.prototype.getInfo = function () { Application.prototype.getInfo = function () {
return $http.get(this.url + '/info').error(new AuthInterceptor(this)); return $http.get('api/applications/' + this.id + '/info').error(new AuthInterceptor(this));
}; };
Application.prototype.getMetrics = function () { Application.prototype.getMetrics = function () {
return $http.get(this.url + '/metrics').error(new AuthInterceptor(this)); return $http.get('api/applications/' + this.id + '/metrics').error(new AuthInterceptor(this));
}; };
Application.prototype.getEnv = function () { Application.prototype.getEnv = function () {
return $http.get(this.url + '/env').error(new AuthInterceptor(this)); return $http.get('api/applications/' + this.id + '/env').error(new AuthInterceptor(this));
}; };
Application.prototype.getThreadDump = function () { Application.prototype.getThreadDump = function () {
return $http.get(this.url + '/dump').error(new AuthInterceptor(this)); return $http.get('api/applications/' + this.id + '/dump').error(new AuthInterceptor(this));
}; };
Application.prototype.getTraces = function () { Application.prototype.getTraces = function () {
return $http.get(this.url + '/trace').error(new AuthInterceptor(this)); return $http.get('api/applications/' + this.id + '/trace').error(new AuthInterceptor(this));
}; };
Application.prototype.hasLogfile = function () { Application.prototype.hasLogfile = function () {
return $http.head(this.url + '/logfile').error(new AuthInterceptor(this)); return $http.head('api/applications/' + this.id + '/logfile').error(new AuthInterceptor(this));
}; };
return Application; return Application;
......
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
module.exports = function ($rootScope, Abbreviator, jolokia) { module.exports = function ($rootScope, Abbreviator, jolokia) {
this.list = function (app) { this.list = function (app) {
return jolokia.list(app.url + '/jolokia/') return jolokia.list('/api/applications/' + app.id + '/jolokia/')
.then(function (response) { .then(function (response) {
var domains = []; var domains = [];
for (var rDomainName in response.value) { for (var rDomainName in response.value) {
...@@ -81,14 +81,14 @@ module.exports = function ($rootScope, Abbreviator, jolokia) { ...@@ -81,14 +81,14 @@ module.exports = function ($rootScope, Abbreviator, jolokia) {
}; };
this.readAllAttr = function (app, bean) { this.readAllAttr = function (app, bean) {
return jolokia.read(app.url + '/jolokia/', bean.id); return jolokia.read('/api/applications/' + app.id + '/jolokia/', bean.id);
}; };
this.writeAttr = function (app, bean, attr, val) { this.writeAttr = function (app, bean, attr, val) {
return jolokia.writeAttr(app.url + '/jolokia/', bean.id, attr, val); return jolokia.writeAttr('/api/applications/' + app.id + '/jolokia/', bean.id, attr, val);
}; };
this.invoke = function (app, bean, opname, args) { this.invoke = function (app, bean, opname, args) {
return jolokia.exec(app.url + '/jolokia/', bean.id, opname, args); return jolokia.exec('/api/applications/' + app.id + '/jolokia/', bean.id, opname, args);
}; };
}; };
...@@ -29,16 +29,16 @@ module.exports = function ($http, jolokia) { ...@@ -29,16 +29,16 @@ module.exports = function ($http, jolokia) {
arguments: [loggers[j].name] arguments: [loggers[j].name]
}); });
} }
return jolokia.bulkRequest(app.url + '/jolokia/', requests); return jolokia.bulkRequest('/api/applications/' + app.id + '/jolokia/', requests);
}; };
this.setLoglevel = function (app, logger, level) { this.setLoglevel = function (app, logger, level) {
return jolokia.exec(app.url + '/jolokia/', LOGBACK_MBEAN, 'setLoggerLevel', [logger, return jolokia.exec('/api/applications/' + app.id + '/jolokia/', LOGBACK_MBEAN, 'setLoggerLevel', [logger,
level level
]); ]);
}; };
this.getAllLoggers = function (app) { this.getAllLoggers = function (app) {
return jolokia.readAttr(app.url + '/jolokia/', LOGBACK_MBEAN, 'LoggerList'); return jolokia.readAttr('/api/applications/' + app.id + '/jolokia/', LOGBACK_MBEAN, 'LoggerList');
}; };
}; };
{ {
"name": "spring-boot-admin-server-ui", "name": "spring-boot-admin-server-ui",
"version": "1.1.1", "version": "1.1.3",
"scripts": { "scripts": {
"postinstall": "./node_modules/protractor/bin/webdriver-manager update", "postinstall": "./node_modules/protractor/bin/webdriver-manager update",
"pretest": "./node_modules/protractor/bin/webdriver-manager start &", "pretest": "./node_modules/protractor/bin/webdriver-manager start &",
......
...@@ -25,6 +25,42 @@ ...@@ -25,6 +25,42 @@
<groupId>org.apache.commons</groupId> <groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId> <artifactId>commons-lang3</artifactId>
</dependency> </dependency>
<!-- Use Zuul WITHOUT Hystrix/Ribbon/Config Client -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zuul</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-hystrix</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-ribbon</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-netflix-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-commons</artifactId>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.3.6</version>
</dependency>
<!-- Hazelcast-Support -->
<dependency> <dependency>
<groupId>com.hazelcast</groupId> <groupId>com.hazelcast</groupId>
<artifactId>hazelcast</artifactId> <artifactId>hazelcast</artifactId>
...@@ -34,7 +70,7 @@ ...@@ -34,7 +70,7 @@
<groupId>com.hazelcast</groupId> <groupId>com.hazelcast</groupId>
<artifactId>hazelcast-spring</artifactId> <artifactId>hazelcast-spring</artifactId>
<optional>true</optional> <optional>true</optional>
</dependency> </dependency>
<!-- Test --> <!-- Test -->
<dependency> <dependency>
<groupId>junit</groupId> <groupId>junit</groupId>
......
...@@ -16,12 +16,33 @@ ...@@ -16,12 +16,33 @@
package de.codecentric.boot.admin.config; package de.codecentric.boot.admin.config;
import java.util.List; import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.actuate.endpoint.Endpoint;
import org.springframework.boot.actuate.trace.TraceRepository;
import org.springframework.boot.autoconfigure.AutoConfigureBefore; import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
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.RouteLocator;
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;
import org.springframework.cloud.netflix.zuul.web.ZuulHandlerMapping;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter;
...@@ -29,9 +50,13 @@ import org.springframework.http.converter.json.MappingJackson2HttpMessageConvert ...@@ -29,9 +50,13 @@ import org.springframework.http.converter.json.MappingJackson2HttpMessageConvert
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import com.hazelcast.config.Config; import com.hazelcast.config.Config;
import com.hazelcast.core.EntryEvent;
import com.hazelcast.core.EntryListener;
import com.hazelcast.core.Hazelcast; import com.hazelcast.core.Hazelcast;
import com.hazelcast.core.HazelcastInstance; import com.hazelcast.core.HazelcastInstance;
import com.hazelcast.core.IMap; import com.hazelcast.core.IMap;
import com.hazelcast.core.MapEvent;
import com.netflix.zuul.ZuulFilter;
import de.codecentric.boot.admin.controller.RegistryController; import de.codecentric.boot.admin.controller.RegistryController;
import de.codecentric.boot.admin.model.Application; import de.codecentric.boot.admin.model.Application;
...@@ -41,6 +66,8 @@ import de.codecentric.boot.admin.registry.HashingApplicationUrlIdGenerator; ...@@ -41,6 +66,8 @@ import de.codecentric.boot.admin.registry.HashingApplicationUrlIdGenerator;
import de.codecentric.boot.admin.registry.store.ApplicationStore; import de.codecentric.boot.admin.registry.store.ApplicationStore;
import de.codecentric.boot.admin.registry.store.HazelcastApplicationStore; import de.codecentric.boot.admin.registry.store.HazelcastApplicationStore;
import de.codecentric.boot.admin.registry.store.SimpleApplicationStore; import de.codecentric.boot.admin.registry.store.SimpleApplicationStore;
import de.codecentric.boot.admin.zuul.ApplicationRouteLocator;
import de.codecentric.boot.admin.zuul.ApplicationRouteRefreshListener;
@Configuration @Configuration
public class WebappConfig extends WebMvcConfigurerAdapter { public class WebappConfig extends WebMvcConfigurerAdapter {
...@@ -89,6 +116,115 @@ public class WebappConfig extends WebMvcConfigurerAdapter { ...@@ -89,6 +116,115 @@ public class WebappConfig extends WebMvcConfigurerAdapter {
} }
@Configuration @Configuration
@EnableConfigurationProperties(ZuulProperties.class)
public static class RevereseZuulProxyConfiguration {
@Autowired(required = false)
private TraceRepository traces;
@Autowired
private ZuulProperties zuulProperties;
@Autowired
private ServerProperties server;
@Autowired
private ApplicationRegistry registry;
@Bean
public ApplicationRouteLocator routeLocator() {
return new ApplicationRouteLocator(this.server.getServletPrefix(), registry, this.zuulProperties,
RegistryController.PATH);
}
@Bean
public PreDecorationFilter preDecorationFilter() {
return new PreDecorationFilter(routeLocator(), this.zuulProperties.isAddProxyHeaders());
}
@Bean
public SimpleHostRoutingFilter simpleHostRoutingFilter() {
ProxyRequestHelper helper = new ProxyRequestHelper();
if (this.traces != null) {
helper.setTraces(this.traces);
}
return new SimpleHostRoutingFilter(helper);
}
@Bean
public ZuulController zuulController() {
return new ZuulController();
}
@Bean
public ZuulHandlerMapping zuulHandlerMapping(RouteLocator routes) {
return new ZuulHandlerMapping(routes, zuulController());
}
// pre filters
@Bean
public FormBodyWrapperFilter formBodyWrapperFilter() {
return new FormBodyWrapperFilter();
}
@Bean
public DebugFilter debugFilter() {
return new DebugFilter();
}
@Bean
public Servlet30WrapperFilter servlet30WrapperFilter() {
return new Servlet30WrapperFilter();
}
// post filters
@Bean
public SendResponseFilter sendResponseFilter() {
return new SendResponseFilter();
}
@Bean
public SendErrorFilter sendErrorFilter() {
return new SendErrorFilter();
}
@Configuration
protected static class ZuulFilterConfiguration {
@Autowired
private Map<String, ZuulFilter> filters;
@Bean
public ZuulFilterInitializer zuulFilterInitializer() {
return new ZuulFilterInitializer(this.filters);
}
}
@Bean
ApplicationRouteRefreshListener applicationRouteRefreshListener() {
return new ApplicationRouteRefreshListener(routeLocator(), zuulHandlerMapping(routeLocator()));
}
@Configuration
@ConditionalOnClass(Endpoint.class)
protected static class RoutesEndpointConfiguration {
@Autowired
private ProxyRouteLocator routeLocator;
@Bean
public RoutesEndpoint zuulEndpoint() {
return new RoutesEndpoint(this.routeLocator);
}
}
}
@Configuration
@ConditionalOnClass({ Hazelcast.class }) @ConditionalOnClass({ Hazelcast.class })
@ConditionalOnExpression("${spring.boot.admin.hazelcast.enable:true}") @ConditionalOnExpression("${spring.boot.admin.hazelcast.enable:true}")
@AutoConfigureBefore(SimpleConfig.class) @AutoConfigureBefore(SimpleConfig.class)
...@@ -114,8 +250,57 @@ public class WebappConfig extends WebMvcConfigurerAdapter { ...@@ -114,8 +250,57 @@ public class WebappConfig extends WebMvcConfigurerAdapter {
public ApplicationStore applicationStore(HazelcastInstance hazelcast) { public ApplicationStore applicationStore(HazelcastInstance hazelcast) {
IMap<String, Application> map = hazelcast.<String, Application> getMap(hazelcastMapName); IMap<String, Application> map = hazelcast.<String, Application> getMap(hazelcastMapName);
map.addIndex("name", false); map.addIndex("name", false);
map.addEntryListener(entryListener(), false);
return new HazelcastApplicationStore(map); return new HazelcastApplicationStore(map);
} }
@Bean
public EntryListener<String, Application> entryListener() {
return new ApplicationEntryListener();
}
private static class ApplicationEntryListener implements EntryListener<String, Application> {
@Autowired
private ZuulHandlerMapping zuulHandlerMapping;
@Autowired
private ApplicationRouteLocator routeLocator;
private void reset() {
routeLocator.resetRoutes();
zuulHandlerMapping.registerHandlers();
}
@Override
public void entryAdded(EntryEvent<String, Application> event) {
reset();
}
@Override
public void entryRemoved(EntryEvent<String, Application> event) {
reset();
}
@Override
public void entryUpdated(EntryEvent<String, Application> event) {
reset();
}
@Override
public void entryEvicted(EntryEvent<String, Application> event) {
reset();
}
@Override
public void mapEvicted(MapEvent event) {
reset();
}
@Override
public void mapCleared(MapEvent event) {
reset();
}
}
} }
} }
...@@ -36,8 +36,9 @@ import de.codecentric.boot.admin.registry.ApplicationRegistryConflictException; ...@@ -36,8 +36,9 @@ import de.codecentric.boot.admin.registry.ApplicationRegistryConflictException;
* REST controller for controlling registration of managed applications. * REST controller for controlling registration of managed applications.
*/ */
@RestController @RestController
@RequestMapping(value = "/api/applications") @RequestMapping(value = RegistryController.PATH)
public class RegistryController { public class RegistryController {
public static final String PATH = "/api/applications";
private static final Logger LOGGER = LoggerFactory.getLogger(RegistryController.class); private static final Logger LOGGER = LoggerFactory.getLogger(RegistryController.class);
......
package de.codecentric.boot.admin.event;
import org.springframework.context.ApplicationEvent;
import de.codecentric.boot.admin.model.Application;
/**
* Abstract Event regearding spring boot admin clients
* @author Johannes Stelzer
*/
public abstract class ClientApplicationEvent extends ApplicationEvent {
private static final long serialVersionUID = 1L;
private final Application application;
public ClientApplicationEvent(Object source, Application application) {
super(source);
this.application = application;
}
public Application getApplication() {
return application;
}
}
package de.codecentric.boot.admin.event;
import de.codecentric.boot.admin.model.Application;
public class ClientApplicationRegisteredEvent extends ClientApplicationEvent {
public ClientApplicationRegisteredEvent(Object source, Application application) {
super(source, application);
}
private static final long serialVersionUID = 1L;
}
package de.codecentric.boot.admin.event;
import de.codecentric.boot.admin.model.Application;
public class ClientApplicationUnregisteredEvent extends ClientApplicationEvent {
public ClientApplicationUnregisteredEvent(Object source, Application application) {
super(source, application);
}
private static final long serialVersionUID = 1L;
}
...@@ -22,7 +22,11 @@ import java.util.Collection; ...@@ -22,7 +22,11 @@ import java.util.Collection;
import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.Validate;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import de.codecentric.boot.admin.event.ClientApplicationRegisteredEvent;
import de.codecentric.boot.admin.event.ClientApplicationUnregisteredEvent;
import de.codecentric.boot.admin.model.Application; import de.codecentric.boot.admin.model.Application;
import de.codecentric.boot.admin.registry.store.ApplicationStore; import de.codecentric.boot.admin.registry.store.ApplicationStore;
...@@ -30,11 +34,12 @@ import de.codecentric.boot.admin.registry.store.ApplicationStore; ...@@ -30,11 +34,12 @@ import de.codecentric.boot.admin.registry.store.ApplicationStore;
* Registry for all applications that should be managed/administrated by the Spring Boot Admin application. * Registry for all applications that should be managed/administrated by the Spring Boot Admin application.
* Backed by an ApplicationStore for persistence and an ApplicationIdGenerator for id generation. * Backed by an ApplicationStore for persistence and an ApplicationIdGenerator for id generation.
*/ */
public class ApplicationRegistry { public class ApplicationRegistry implements ApplicationContextAware {
private static final Logger LOGGER = LoggerFactory.getLogger(ApplicationRegistry.class); private static final Logger LOGGER = LoggerFactory.getLogger(ApplicationRegistry.class);
private final ApplicationStore store; private final ApplicationStore store;
private final ApplicationIdGenerator generator; private final ApplicationIdGenerator generator;
private ApplicationContext context;
public ApplicationRegistry(ApplicationStore store, ApplicationIdGenerator generator) { public ApplicationRegistry(ApplicationStore store, ApplicationIdGenerator generator) {
this.store = store; this.store = store;
...@@ -59,6 +64,7 @@ public class ApplicationRegistry { ...@@ -59,6 +64,7 @@ public class ApplicationRegistry {
if (oldApp == null) { if (oldApp == null) {
LOGGER.info("New Application {} registered ", newApp); LOGGER.info("New Application {} registered ", newApp);
context.publishEvent(new ClientApplicationRegisteredEvent(this, newApp));
} else { } else {
if ((app.getUrl().equals(oldApp.getUrl()) && app.getName().equals(oldApp.getName()))) { if ((app.getUrl().equals(oldApp.getUrl()) && app.getName().equals(oldApp.getName()))) {
LOGGER.debug("Application {} refreshed", newApp); LOGGER.debug("Application {} refreshed", newApp);
...@@ -121,7 +127,15 @@ public class ApplicationRegistry { ...@@ -121,7 +127,15 @@ public class ApplicationRegistry {
*/ */
public Application unregister(String id) { public Application unregister(String id) {
Application app = store.delete(id); Application app = store.delete(id);
LOGGER.info("Application {} unregistered ", app); if (app != null) {
LOGGER.info("Application {} unregistered ", app);
context.publishEvent(new ClientApplicationUnregisteredEvent(this, app));
}
return app; return app;
} }
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
this.context = applicationContext;
}
} }
package de.codecentric.boot.admin.zuul;
import java.util.LinkedHashMap;
import org.springframework.cloud.netflix.zuul.filters.ProxyRouteLocator;
import org.springframework.cloud.netflix.zuul.filters.ZuulProperties;
import org.springframework.cloud.netflix.zuul.filters.ZuulProperties.ZuulRoute;
import de.codecentric.boot.admin.model.Application;
import de.codecentric.boot.admin.registry.ApplicationRegistry;
public class ApplicationRouteLocator extends ProxyRouteLocator {
private ApplicationRegistry registry;
private String prefix;
public ApplicationRouteLocator(String servletPath, ApplicationRegistry registry, ZuulProperties properties,
String prefix) {
super(servletPath, null, properties);
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()) {
String key = prefix + "/" + application.getId() + "/*/**";
locateRoutes.put(key, new ZuulRoute(key, application.getUrl()));
}
}
return locateRoutes;
}
}
package de.codecentric.boot.admin.zuul;
import org.springframework.cloud.netflix.zuul.web.ZuulHandlerMapping;
import org.springframework.context.ApplicationListener;
import de.codecentric.boot.admin.event.ClientApplicationEvent;
public class ApplicationRouteRefreshListener implements ApplicationListener<ClientApplicationEvent> {
private final ApplicationRouteLocator routeLocator;
private final ZuulHandlerMapping zuulHandlerMapping;
public ApplicationRouteRefreshListener(ApplicationRouteLocator routeLocator, ZuulHandlerMapping zuulHandlerMapping) {
this.routeLocator = routeLocator;
this.zuulHandlerMapping = zuulHandlerMapping;
}
@Override
public void onApplicationEvent(ClientApplicationEvent event) {
this.routeLocator.resetRoutes();
this.zuulHandlerMapping.registerHandlers();
}
}
...@@ -70,8 +70,8 @@ public class AdminApplicationHazelcastTest { ...@@ -70,8 +70,8 @@ public class AdminApplicationHazelcastTest {
@After @After
public void shutdown() { public void shutdown() {
instance1.stop(); instance1.close();
instance2.stop(); instance2.close();
} }
@Test @Test
......
...@@ -18,38 +18,35 @@ package de.codecentric.boot.admin; ...@@ -18,38 +18,35 @@ package de.codecentric.boot.admin;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import java.util.List; import java.util.List;
import java.util.Map;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.IntegrationTest;
import org.springframework.boot.test.SpringApplicationConfiguration; import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.boot.test.TestRestTemplate; import org.springframework.boot.test.TestRestTemplate;
import org.springframework.boot.test.WebIntegrationTest;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.web.client.RestTemplate;
import de.codecentric.boot.admin.AdminApplicationTest.TestAdminApplication; import de.codecentric.boot.admin.AdminApplicationTest.TestAdminApplication;
import de.codecentric.boot.admin.config.EnableAdminServer; import de.codecentric.boot.admin.config.EnableAdminServer;
import de.codecentric.boot.admin.model.Application;
/** /**
* *
* Integration test to verify the correct functionality of the REST API. * Integration test to verify the correct functionality of the REST API.
* *
* @author Dennis Schulte * @author Dennis Schulte
*/ */
@RunWith(SpringJUnit4ClassRunner.class) @RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = TestAdminApplication.class) @SpringApplicationConfiguration(classes = TestAdminApplication.class)
@WebAppConfiguration @WebIntegrationTest({ "server.port=0" })
@IntegrationTest({ "server.port=0" })
public class AdminApplicationTest { public class AdminApplicationTest {
RestTemplate restTemplate = new TestRestTemplate();
@Value("${local.server.port}") @Value("${local.server.port}")
private int port = 0; private int port = 0;
...@@ -61,6 +58,20 @@ public class AdminApplicationTest { ...@@ -61,6 +58,20 @@ public class AdminApplicationTest {
assertEquals(HttpStatus.OK, entity.getStatusCode()); assertEquals(HttpStatus.OK, entity.getStatusCode());
} }
@Test
public void testReverseProxy() {
String apiBaseUrl = "http://localhost:" + port + "/api/applications";
ResponseEntity<Application> entity = new TestRestTemplate().postForEntity(apiBaseUrl, new Application(
"http://localhost:" + port, "TestApp"), Application.class);
@SuppressWarnings("rawtypes")
ResponseEntity<Map> health = new TestRestTemplate().getForEntity(apiBaseUrl + "/" + entity.getBody().getId()
+ "/info", Map.class);
assertEquals(HttpStatus.OK, health.getStatusCode());
}
@Configuration @Configuration
@EnableAutoConfiguration @EnableAutoConfiguration
@EnableAdminServer @EnableAdminServer
......
...@@ -23,6 +23,8 @@ import java.util.Collection; ...@@ -23,6 +23,8 @@ import java.util.Collection;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.context.ApplicationContext;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
...@@ -34,11 +36,14 @@ import de.codecentric.boot.admin.registry.store.SimpleApplicationStore; ...@@ -34,11 +36,14 @@ import de.codecentric.boot.admin.registry.store.SimpleApplicationStore;
public class RegistryControllerTest { public class RegistryControllerTest {
private RegistryController controller; private RegistryController controller;
private ApplicationRegistry registry;
@Before @Before
public void setup() { public void setup() {
controller = new RegistryController(new ApplicationRegistry(new SimpleApplicationStore(), registry = new ApplicationRegistry(new SimpleApplicationStore(),
new HashingApplicationUrlIdGenerator())); new HashingApplicationUrlIdGenerator());
registry.setApplicationContext(Mockito.mock(ApplicationContext.class));
controller = new RegistryController(registry);
} }
@Test @Test
......
...@@ -23,6 +23,8 @@ import static org.junit.Assert.assertTrue; ...@@ -23,6 +23,8 @@ import static org.junit.Assert.assertTrue;
import java.util.Collection; import java.util.Collection;
import org.junit.Test; import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.context.ApplicationContext;
import de.codecentric.boot.admin.model.Application; import de.codecentric.boot.admin.model.Application;
import de.codecentric.boot.admin.registry.store.SimpleApplicationStore; import de.codecentric.boot.admin.registry.store.SimpleApplicationStore;
...@@ -32,6 +34,10 @@ public class ApplicationRegistryTest { ...@@ -32,6 +34,10 @@ public class ApplicationRegistryTest {
private ApplicationRegistry registry = new ApplicationRegistry(new SimpleApplicationStore(), private ApplicationRegistry registry = new ApplicationRegistry(new SimpleApplicationStore(),
new HashingApplicationUrlIdGenerator()); new HashingApplicationUrlIdGenerator());
public ApplicationRegistryTest() {
registry.setApplicationContext(Mockito.mock(ApplicationContext.class));
}
@Test(expected = NullPointerException.class) @Test(expected = NullPointerException.class)
public void registerFailed1() throws Exception { public void registerFailed1() throws Exception {
registry.register(new Application(null, null)); registry.register(new Application(null, null));
......
...@@ -15,34 +15,22 @@ ...@@ -15,34 +15,22 @@
*/ */
package de.codecentric.boot.admin.config; package de.codecentric.boot.admin.config;
import java.lang.reflect.Field;
import java.util.Arrays; import java.util.Arrays;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.actuate.endpoint.mvc.EndpointHandlerMapping;
import org.springframework.boot.actuate.endpoint.mvc.EndpointHandlerMappingCustomizer;
import org.springframework.boot.actuate.endpoint.mvc.JolokiaMvcEndpoint;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.embedded.EmbeddedServletContainerInitializedEvent;
import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.ClientHttpRequestInterceptor; import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.scheduling.config.ScheduledTaskRegistrar; import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.util.ReflectionUtils;
import org.springframework.web.client.RestTemplate; import org.springframework.web.client.RestTemplate;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.mvc.ServletWrappingController;
import de.codecentric.boot.admin.actuate.LogfileMvcEndpoint; import de.codecentric.boot.admin.actuate.LogfileMvcEndpoint;
import de.codecentric.boot.admin.services.SpringBootAdminRegistrator; import de.codecentric.boot.admin.services.SpringBootAdminRegistrator;
import de.codecentric.boot.admin.web.BasicAuthHttpRequestInterceptor; import de.codecentric.boot.admin.web.BasicAuthHttpRequestInterceptor;
import de.codecentric.boot.admin.web.EndpointCorsInterceptor;
/** /**
* This configuration adds a registrator bean to the spring context. This bean checks periodicaly, if the using * This configuration adds a registrator bean to the spring context. This bean checks periodicaly, if the using
...@@ -105,57 +93,4 @@ public class SpringBootAdminClientAutoConfiguration { ...@@ -105,57 +93,4 @@ public class SpringBootAdminClientAutoConfiguration {
} }
} }
@Bean
protected EndpointCorsInterceptor endpointCorsInterceptor() {
return new EndpointCorsInterceptor();
}
@Bean
protected EndpointHandlerMappingCustomizer endpointHandlerMappingCustomizer() {
return new EndpointHandlerMappingCustomizer() {
@Override
public void customize(EndpointHandlerMapping mapping) {
mapping.setInterceptors(new Object[] { endpointCorsInterceptor() });
}
};
}
@Autowired
private ApplicationContext applicationContext;
@Bean
public ApplicationListener<EmbeddedServletContainerInitializedEvent> appListener() {
/*
* Set jolokias AgentServlet to support Options request and the Dispatcher servlet
* to forward such. Done in this nasty way in case a second servlet-container is
* spun up, when management.port != server.port Also @see
* https://github.com/spring-projects/spring-boot/issues/1987
*/
return new ApplicationListener<EmbeddedServletContainerInitializedEvent>() {
@Override
public void onApplicationEvent(EmbeddedServletContainerInitializedEvent event) {
// set DispatcherServlet to forward OptionsRequest
for (DispatcherServlet servlet : event.getApplicationContext()
.getBeansOfType(DispatcherServlet.class).values()) {
servlet.setDispatchOptionsRequest(true);
}
// set Jolokias ServletWrappingController to support OPTIONS
for (JolokiaMvcEndpoint jolokiaMvcEndpoint : SpringBootAdminClientAutoConfiguration.this.applicationContext
.getBeansOfType(JolokiaMvcEndpoint.class).values()) {
try {
Field controllerField = JolokiaMvcEndpoint.class.getDeclaredField("controller");
ReflectionUtils.makeAccessible(controllerField);
ServletWrappingController controller = (ServletWrappingController) controllerField
.get(jolokiaMvcEndpoint);
controller.setSupportedMethods("GET", "HEAD", "POST", "OPTIONS");
}
catch (Exception ex) {
throw new RuntimeException("Couldn't reconfigure servletWrappingController for Jolokia", ex);
}
}
}
};
}
} }
/*
* 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.web;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
public class EndpointCorsInterceptor extends HandlerInterceptorAdapter {
// Configurable origin for CORS - default: * (all)
@Value("${http.filter.cors.origin:*}")
private String origin;
@Value("${http.filter.cors.headers:Origin, X-Requested-With, Content-Type, Accept}")
private String headers;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
response.setHeader("Access-Control-Allow-Origin", origin);
response.setHeader("Access-Control-Allow-Headers", headers);
return super.preHandle(request, response, handler);
}
public void setOrigin(String origin) {
this.origin = origin;
}
public String getOrigin() {
return origin;
}
public String getHeaders() {
return headers;
}
public void setHeaders(String headers) {
this.headers = headers;
}
}
/*
* 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.web;
import static org.junit.Assert.assertEquals;
import java.util.Arrays;
import java.util.Map;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.IntegrationTest;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.boot.test.TestRestTemplate;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import de.codecentric.boot.admin.web.CorsFilterOnDifferentPortsTest.TestAdminApplication;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = TestAdminApplication.class)
@WebAppConfiguration
@IntegrationTest({ "server.port=0", "management.port=0", "spring.boot.admin.url=http://localhost:65000" })
public class CorsFilterOnDifferentPortsTest {
RestTemplate restTemplate = new TestRestTemplate();
@Value("${local.server.port}")
private int serverPort = 0;
@Value("${local.management.port}")
private int managementPort = 0;
@Test
@SuppressWarnings("rawtypes")
public void testCORS_GET_info_endpoint() {
// DO serve CORS-Headers on management-endpoints
ResponseEntity<Map> info = new TestRestTemplate().getForEntity("http://localhost:" + managementPort + "/info",
Map.class);
assertEquals(HttpStatus.OK, info.getStatusCode());
assertEquals(Arrays.asList("*"), info.getHeaders().get("Access-Control-Allow-Origin"));
assertEquals(Arrays.asList("Origin, X-Requested-With, Content-Type, Accept"),
info.getHeaders().get("Access-Control-Allow-Headers"));
}
@Test
public void testCORS_OPTIONS_jolokia_endpoint() {
// DO serve CORS-Headers on management-endpoints
ResponseEntity<Void> options = new TestRestTemplate().exchange("http://localhost:" + managementPort
+ "/jolokia",
HttpMethod.OPTIONS, HttpEntity.EMPTY, Void.class);
assertEquals(HttpStatus.OK, options.getStatusCode());
assertEquals(Arrays.asList("*"), options.getHeaders().get("Access-Control-Allow-Origin"));
assertEquals(Arrays.asList("Origin, X-Requested-With, Content-Type, Accept"),
options.getHeaders().get("Access-Control-Allow-Headers"));
}
@Test
public void testCORS_GET_application() {
// DO NOT serve CORS-Headers on application-endpoints
ResponseEntity<String> hello = new TestRestTemplate().getForEntity("http://localhost:" + serverPort + "/hello",
String.class);
assertEquals(HttpStatus.OK, hello.getStatusCode());
assertEquals(null, hello.getHeaders().get("Access-Control-Allow-Origin"));
assertEquals(null, hello.getHeaders().get("Access-Control-Allow-Headers"));
}
@Test
public void testCORS_OPTIONS_application() {
// DO NOT serve CORS-Headers on application-endpoints
ResponseEntity<Void> options = new TestRestTemplate().exchange("http://localhost:" + serverPort + "/hello",
HttpMethod.OPTIONS, HttpEntity.EMPTY, Void.class);
assertEquals(HttpStatus.OK, options.getStatusCode());
assertEquals(null, options.getHeaders().get("Access-Control-Allow-Origin"));
assertEquals(null, options.getHeaders().get("Access-Control-Allow-Headers"));
}
@Configuration
@EnableAutoConfiguration
@RestController
public static class TestAdminApplication {
@RequestMapping("/hello")
public String hello() {
return "hello world!";
}
}
}
\ No newline at end of file
/*
* 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.web;
import static org.junit.Assert.assertEquals;
import java.util.Arrays;
import java.util.Map;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.IntegrationTest;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.boot.test.TestRestTemplate;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import de.codecentric.boot.admin.web.CorsFilterOnSamePortsTest.TestAdminApplication;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = TestAdminApplication.class)
@WebAppConfiguration
@IntegrationTest({ "server.port=0", "spring.boot.admin.url=http://localhost:65000" })
public class CorsFilterOnSamePortsTest {
RestTemplate restTemplate = new TestRestTemplate();
@Value("${local.server.port}")
private int serverPort = 0;
@Test
@SuppressWarnings("rawtypes")
public void testCORS_GET_info_endpoint() {
// DO serve CORS-Headers on management-endpoints
ResponseEntity<Map> info = new TestRestTemplate().getForEntity("http://localhost:" + serverPort + "/info",
Map.class);
assertEquals(HttpStatus.OK, info.getStatusCode());
assertEquals(Arrays.asList("*"), info.getHeaders().get("Access-Control-Allow-Origin"));
assertEquals(Arrays.asList("Origin, X-Requested-With, Content-Type, Accept"),
info.getHeaders().get("Access-Control-Allow-Headers"));
}
@Test
public void testCORS_OPTIONS_jolokia_endpoint() {
// DO serve CORS-Headers on management-endpoints
ResponseEntity<Void> options = new TestRestTemplate().exchange("http://localhost:" + serverPort
+ "/jolokia",
HttpMethod.OPTIONS, HttpEntity.EMPTY, Void.class);
assertEquals(HttpStatus.OK, options.getStatusCode());
assertEquals(Arrays.asList("*"), options.getHeaders().get("Access-Control-Allow-Origin"));
assertEquals(Arrays.asList("Origin, X-Requested-With, Content-Type, Accept"),
options.getHeaders().get("Access-Control-Allow-Headers"));
}
@Test
public void testCORS_GET_application() {
// DO NOT serve CORS-Headers on application-endpoints
ResponseEntity<String> hello = new TestRestTemplate().getForEntity("http://localhost:" + serverPort + "/hello",
String.class);
assertEquals(HttpStatus.OK, hello.getStatusCode());
assertEquals(null, hello.getHeaders().get("Access-Control-Allow-Origin"));
assertEquals(null, hello.getHeaders().get("Access-Control-Allow-Headers"));
}
@Test
public void testCORS_OPTIONS_application() {
// DO NOT serve CORS-Headers on application-endpoints
ResponseEntity<Void> options = new TestRestTemplate().exchange("http://localhost:" + serverPort + "/hello",
HttpMethod.OPTIONS, HttpEntity.EMPTY, Void.class);
assertEquals(HttpStatus.OK, options.getStatusCode());
assertEquals(null, options.getHeaders().get("Access-Control-Allow-Origin"));
assertEquals(null, options.getHeaders().get("Access-Control-Allow-Headers"));
}
@Configuration
@EnableAutoConfiguration
@RestController
public static class TestAdminApplication {
@RequestMapping("/hello")
public String hello() {
return "hello world!";
}
}
}
\ No newline at end of file
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