Commit 38a5cb25 by Johannes Edmeier

Add option to submit credentials when registering

With this change it is possible to include credentials in the instance metadata which will then be used to access the client endpoints using HTTP Basic authorization. closes #359
parent 21c17900
......@@ -4,7 +4,7 @@
[[show-version-in-application-list]]
=== Show version in application list ===
To get the version show up in the admin's application list you can use the `build-info` goal from the `spring-boot-maven-plugin`, which generates the `META-INF/build-info.properties`. See also the http://docs.spring.io/spring-boot/docs/current-SNAPSHOT/reference/htmlsingle/#howto-build-info[Spring Boot Reference Guide].
To have the version show up in the application list you can use the `build-info` goal from the `spring-boot-maven-plugin`, which generates the `META-INF/build-info.properties`. See also the http://docs.spring.io/spring-boot/docs/current-SNAPSHOT/reference/htmlsingle/#howto-build-info[Spring Boot Reference Guide].
[source,xml]
.pom.xml
......@@ -56,9 +56,9 @@ NOTE: In case you are deploying multiple applications to the same JVM and multip
[[spring-boot-admin-client]]
=== Spring Boot Admin Client ===
The Spring Boot Admin Client registers the application at the admin server. This is done by periodically doing a http post-request to the admin server providing information about the application. It also adds Jolokia to your dependencies, so that JMX-beans are accessible via http, this is needed if you want to manage loglevels or JMX-beans via the admin UI.
The Spring Boot Admin Client registers the application at the admin server. This is done by periodically doing a HTTP post request to the SBA Server providing information about the application. It also adds Jolokia to your application, so that JMX-beans are accessible via HTTP.
There are plenty of properties to influence the way how the client registers your application. In case that doesn't fit your needs, you can provide your own `AppliationFactory` implementation.
TIP: There are plenty of properties to influence the way how the SBA Client registers your application. In case that doesn't fit your needs, you can provide your own `AppliationFactory` implementation.
.Spring Boot Admin Client configuration options
|===
......@@ -76,9 +76,9 @@ There are plenty of properties to influence the way how the client registers you
| Http-path of registration endpoint at your admin server.
| `"api/applications"`
| spring.boot.admin.username
| spring.boot.admin.username +
spring.boot.admin.password
| Username and password for http-basic authentication. If set the registration uses http-basic-authentication when registering at the admin server.
| Username and password in case the SBA Server api is protected with HTTP Basic authentication.
|
| spring.boot.admin.period
......@@ -118,8 +118,16 @@ spring.boot.admin.password
| `false`
| spring.boot.admin.client.metadata.*
| Metadata to be asscoiated with this instance
| Metadata key-value-pairs to be asscoiated with this instance.
|
|===
----
\ No newline at end of file
.Instance metadata options
|===
| Key |Value |Default value
| user.name +
user.password
| Credentials being used to access the endpoints.
|
|===
[[getting-started]]
== Getting started ==
[[set-up-admin-server]]
=== Set up admin server ===
=== Setting up Spring Boot Admin Server ===
First you need to setup your server. To do this just setup a simple boot project (using http://start.spring.io for example).
......@@ -24,7 +23,7 @@ First you need to setup your server. To do this just setup a simple boot project
</dependency>
----
. Pull in the Boot Admin Server configuration via adding `@EnableAdminServer` to your configuration:
. Pull in the Spring Boot Admin Server configuration via adding `@EnableAdminServer` to your configuration:
+
[source,java]
----
......@@ -38,21 +37,19 @@ public class SpringBootAdminApplication {
}
----
NOTE: If you want to setup the Spring Boot Admin Server via war-deployment in a servlet-container, please have a look at the https://github.com/codecentric/spring-boot-admin/blob/master/spring-boot-admin-samples/spring-boot-admin-sample-war/[spring-boot-admin-sample-war].
TIP: If you want to setup the Spring Boot Admin Server via war-deployment in a servlet-container, please have a look at the https://github.com/codecentric/spring-boot-admin/blob/master/spring-boot-admin-samples/spring-boot-admin-sample-war/[spring-boot-admin-sample-war].
See also the https://github.com/codecentric/spring-boot-admin/tree/master/spring-boot-admin-samples/spring-boot-admin-sample/[spring-boot-admin-sample] project.
[[register-client-applications]]
=== Register client applications ===
=== Registering client applications ===
To register your application at the admin server (next referred as "clients").
Either you can include the `spring-boot-admin` client or use http://projects.spring.io/spring-cloud/spring-cloud.html[Spring Cloud Discovery] (e.g. Eureka)
To register your application at the SBA Server you can either include the SBA Client or use http://projects.spring.io/spring-cloud/spring-cloud.html[Spring Cloud Discovery] (e.g. Eureka)
[[register-clients-via-spring-boot-admin]]
==== spring-boot-admin-starter-client ====
Each application that want to register itself to the admin has to include the Spring Boot Admin Client.
Each application that wants to register has to include the Spring Boot Admin Client.
. Add spring-boot-admin-starter-client to your dependencies:
+
......@@ -66,18 +63,20 @@ Each application that want to register itself to the admin has to include the Sp
</dependency>
----
. Trigger the contained AutoConfiguration and tell the client where to find the admin to register at:
. Enable the SBA Client by configuring the URL of the Spring Boot Admin Server:
+
[source,yml]
.application.yml
----
include::{samples-dir}/spring-boot-admin-sample/src/main/resources/application.yml[tags=configuration-sba-client]
----
<1> The URL of the Spring Boot Admin Server to register at.
<2> Since Spring Boot 1.5.x all endpoints are secured by default. For the sake of brevity we're disabling the security for now. Have a look at the <<securing-spring-boot-admin,security section>> on how to deal with secured endpoints.
[[discover-clients-via-spring-cloud-discovery]]
==== Spring Cloud Discovery ====
If you already using Spring Cloud Discovery for your applications you don't have to add the Spring Boot Admin Client to your applications. Just make the Spring Boot Admin Server a DiscoveryClient, the rest is done by our AutoConfiguration.
If you already use Spring Cloud Discovery for your applications you don't need the SBA Client. Just make the Spring Boot Admin Server a DiscoveryClient, the rest is done by our AutoConfiguration.
The following steps are for using Eureka, but other Spring Cloud Discovery implementations are supported as well. There are examples using https://github.com/codecentric/spring-boot-admin/tree/master/spring-boot-admin-samples/spring-boot-admin-sample-consul/[Consul] and https://github.com/codecentric/spring-boot-admin/tree/master/spring-boot-admin-samples/spring-boot-admin-sample-zookeeper/[Zookeeper].
......@@ -105,7 +104,9 @@ include::{samples-dir}/spring-boot-admin-sample-eureka/src/main/java/de/codecent
----
include::{samples-dir}/spring-boot-admin-sample-eureka/src/main/resources/application.yml[tags=configuration-eureka]
----
<1> Configuration section for the Eureka client
<2> Since Spring Boot 1.5.x all endpoints are secured by default. For the sake of brevity we're disabling the security for now. Have a look at the <<securing-spring-boot-admin,security section>> on how to deal with secured endpoints.
See also https://github.com/codecentric/spring-boot-admin/tree/master/spring-boot-admin-samples/spring-boot-admin-sample-eureka/[spring-boot-admin-sample-eureka].
NOTE: You can include the Spring Boot Admin to your Eureka server. Add the dependencies, add `@EnableAdminServer` to your configuration and set `spring.boot.admin.context-path` to something different than `"/"` so that the Spring Boot Admin Server UI won't clash with Eureka's one.
TIP: You can include the Spring Boot Admin Server to your Eureka server. Setup everything as described above and set `spring.boot.admin.context-path` to something different than `"/"` so that the Spring Boot Admin Server UI won't clash with Eureka's one.
......@@ -15,9 +15,9 @@ Johannes Edmeier <https://twitter.com/joshiste[@joshiste]>
== What is Spring Boot Admin? ==
Spring Boot Admin is a simple application to manage and monitor your http://projects.spring.io/spring-boot/[Spring Boot Applications].
The applications register with our Spring Boot Admin Client (via http) or are discovered using Spring Cloud (e.g. Eureka).
The UI is just an Angular.js application on top of the Spring Boot Actuator endpoints. In case you want to use the more advanced features (e.g. jmx-, loglevel-management), Jolokia must be included in the client application.
Spring Boot Admin is a application to manage and monitor your http://projects.spring.io/spring-boot/[Spring Boot Applications].
The applications register with our Spring Boot Admin Client (via HTTP) or are discovered using Spring Cloud (e.g. Eureka).
The UI is just an AngularJs application on top of the Spring Boot Actuator endpoints.
include::getting-started.adoc[]
......@@ -27,4 +27,4 @@ include::server.adoc[]
include::security.adoc[]
include::faqs.adoc[]
\ No newline at end of file
include::faqs.adoc[]
......@@ -4,10 +4,39 @@
=== Securing Spring Boot Admin Server ===
Since there are several approaches on solving authentication and authorization in distributed web applications Spring Boot Admin doesn't ship a default one.
However you can include Spring Security to your Spring Boot Admin Server and configure it the way you like.
However you can include Spring Security to your SBA Server and configure it the way you like.
=== Securing Client's Actuator Endpoints ===
The simplest way to secure your actuator endpoints is to use basic authorization and the same username/password for all applications. This way the browser asks for the credentials and if you set `zuul.senstivieHeaders:` the Zuul Proxy in Spring Boot Admin Server forwards them to the clients.
When the actuator endpoints are secured using HTTP Basic authentication the SBA Server needs credentials to access them. You can submit the credentials in the metadata when registering the application. The `BasicAuthHttpHeaderProvider` then uses this metadata to add the `Authorization` header to access your application's actuator endpoints. You can provide your own `HttpHeadersProvider` to alter the behaviour (e.g. add some decryption) or add extra headers.
For more complex solutions (Spring Session, OAuth2, ...) please have a look at the samples in https://github.com/joshiste/spring-boot-admin-samples[joshiste/spring-boot-admin-samples^].
Submitting the credentials using SBA Client:
[source,yaml]
.application.yml
----
spring.boot.admin:
url: http://localhost:8080
client:
metadata:
user.name: ${security.user.name}
user.password: ${security.user.password}
----
Submitting the credentials using Eureka:
[source,yaml]
.application.yml
----
eureka:
instance:
metadata-map:
user.name: ${security.user.name}
user.password: ${security.user.password}
----
NOTE: The SBA Server masks certain metadata in the HTTP interface to prevent leaking of sensitive information.
WARNING: You should configure HTTPS for your SBA Server or (service registry) when submitting credentials via the metadata.
WARNING: When using Spring Cloud Discovery, you must be aware that anybody who can query your service registry can obtain the credentials.
TIP: When using this approach the SBA Server decides whether or not the user can access the registered applications. There are more complex solutions possible (using OAuth2) to let the clients decide if the user can access the endpoints. For that please have a look at the samples in https://github.com/joshiste/spring-boot-admin-samples[joshiste/spring-boot-admin-samples^].
[[spring-cloud-discovery-support]]
=== Spring Cloud Discovery ===
The Spring Boot Admin Server is capable of using Spring Clouds `DiscoveryClient` to discover applications. The advantage is that the clients don't have to include the `spring-boot-admin-starter-client`. You just have to add a DiscoveryClient to your admin server - everything else is done by AutoConfiguration.
The Spring Boot Admin Server can use Spring Clouds `DiscoveryClient` to discover applications. The advantage is that the clients don't have to include the `spring-boot-admin-starter-client`. You just have to add a DiscoveryClient to your admin server - everything else is done by AutoConfiguration.
The setup is explained <<discover-clients-via-spring-cloud-discovery,above>>.
==== ServiceInstanceConverter ====
The informations from the discovered services are converted by the `ServiceInstanceConverter`. Spring Boot Admin ships with a default and Eureka converter implementation. The correct one is selected by AutoConfiguration. You can use your own conversion by implementing the interface and adding the bean to your application context.
The information from the service registry are converted by the `ServiceInstanceConverter`. Spring Boot Admin ships with a default and Eureka converter implementation. The correct one is selected by AutoConfiguration.
TIP: If you want to customize the default conversion of services you can either add `health.path`, `management.port` and/or `mangament.context-path` entries to the services metadata. This allows you to set the health or management path per application. In case you want to configure this for all of your discovered services, you can use the `spring.boot.admin.discovery.converter.*` properties for your Spring Boot Admin Server configuration. The services' metadata takes precedence over the server configuration. For the health-url the `EurekaServiceInstanceConverter` uses the healthCheckUrl registered in Eureka, which can be set for your client via `eureka.instance.healthCheckUrl`.
TIP: You can modify how the information from the registry is used to register the application by using SBA Server configuration options and instance metadata. The values from the metadata takes precedence over the server config. If the plenty of options don't fit your needs you can provide your own `ServiceInstanceConverter`.
NOTE: When using Eureka, the `healthCheckUrl` known to Eureka is used for health-checking, which can be set on your client using `eureka.instance.healthCheckUrl`.
.Discovery configuration options
|===
......@@ -29,4 +31,26 @@ TIP: If you want to customize the default conversion of services you can either
| spring.boot.admin.discovery.ignored-services
| This services will be ignored when using discovery and not registered as application.
|
|===
\ No newline at end of file
|===
.Instance metadata options
|===
| Key |Value |Default value
| user.name +
user.password
| Credentials being used to access the endpoints.
|
| managment.port
| The port is substituted in the service URL and will be used for accessing the actuator endpoints.
|
| managment.path
| The path is appendend to the service URL and will be used for accessing the actuator endpoints.
| `${spring.boot.admin.discovery.converter.mangement-context-path}`
| health.path
| The path is appendend to the service URL and will be used for the health-checking. Ignored by the `EurekaServiceInstanceConverter`.
| `${spring.boot.admin.discovery.converter.health-endpoint}`
|===
......@@ -10,7 +10,7 @@
|
| spring.boot.admin.monitor.period
| Time interval in ms to update the status of applications with expired status-informations.
| Time interval in ms to update the status of applications with expired status-information.
| 10.000
| spring.boot.admin.monitor.status-lifetime
......
......@@ -5,13 +5,15 @@ spring:
config:
enabled: false
# tag::configuration-eureka[]
eureka:
eureka: #<1>
instance:
leaseRenewalIntervalInSeconds: 10
client:
registryFetchIntervalSeconds: 5
serviceUrl:
defaultZone: ${EUREKA_SERVICE_URL:http://localhost:8761}/eureka/
management.security.enabled: false #<2>
# end::configuration-eureka[]
# tag::configuration-ui-hystrix[]
......@@ -23,3 +25,4 @@ spring.boot.admin.turbine:
clusters: default
url: http://localhost:8989/turbine.stream
# end::configuration-ui-turbine[]
......@@ -11,9 +11,9 @@ spring:
name: @pom.artifactId@
# tag::configuration-sba-client[]
spring.boot.admin.url: http://localhost:8080
spring.boot.admin.url: http://localhost:8080 #<1>
management.security.enabled: false #<2>
# end::configuration-sba-client[]
endpoints:
health:
sensitive: false
endpoints.health.sensitive: false
......@@ -62,6 +62,8 @@ import de.codecentric.boot.admin.registry.store.SimpleApplicationStore;
import de.codecentric.boot.admin.registry.web.RegistryController;
import de.codecentric.boot.admin.web.AdminController;
import de.codecentric.boot.admin.web.PrefixHandlerMapping;
import de.codecentric.boot.admin.web.client.BasicAuthHttpHeaderProvider;
import de.codecentric.boot.admin.web.client.HttpHeadersProvider;
import de.codecentric.boot.admin.web.servlet.resource.ConcatenatingResourceResolver;
import de.codecentric.boot.admin.web.servlet.resource.PreferMinifiedFilteringResourceResolver;
import de.codecentric.boot.admin.web.servlet.resource.ResourcePatternResolvingResourceResolver;
......@@ -186,6 +188,12 @@ public class AdminServerWebConfiguration extends WebMvcConfigurerAdapter
@Bean
@ConditionalOnMissingBean
public HttpHeadersProvider httpHeadersProvider() {
return new BasicAuthHttpHeaderProvider();
}
@Bean
@ConditionalOnMissingBean
public StatusUpdater statusUpdater() {
RestTemplateBuilder builder = restTemplBuilder
.messageConverters(new MappingJackson2HttpMessageConverter())
......@@ -196,7 +204,8 @@ public class AdminServerWebConfiguration extends WebMvcConfigurerAdapter
}
});
StatusUpdater statusUpdater = new StatusUpdater(builder.build(), applicationStore);
StatusUpdater statusUpdater = new StatusUpdater(builder.build(), applicationStore,
httpHeadersProvider());
statusUpdater.setStatusLifetime(adminServerProperties().getMonitor().getStatusLifetime());
return statusUpdater;
}
......
......@@ -41,9 +41,11 @@ import org.springframework.core.annotation.Order;
import de.codecentric.boot.admin.event.RoutesOutdatedEvent;
import de.codecentric.boot.admin.registry.ApplicationRegistry;
import de.codecentric.boot.admin.web.client.HttpHeadersProvider;
import de.codecentric.boot.admin.zuul.ApplicationRouteLocator;
import de.codecentric.boot.admin.zuul.OptionsDispatchingZuulController;
import de.codecentric.boot.admin.zuul.filters.CompositeRouteLocator;
import de.codecentric.boot.admin.zuul.filters.pre.ApplicationHeadersFilter;
import de.codecentric.boot.admin.zuul.filters.route.SimpleHostRoutingFilter;
@Configuration
......@@ -107,6 +109,12 @@ public class RevereseZuulProxyConfiguration extends ZuulConfiguration {
}
@Bean
public ApplicationHeadersFilter applicationHeadersFilter(ApplicationRouteLocator routeLocator,
HttpHeadersProvider headersProvider) {
return new ApplicationHeadersFilter(headersProvider, routeLocator);
}
@Bean
public SimpleHostRoutingFilter simpleHostRoutingFilter() {
return new SimpleHostRoutingFilter(proxyRequestHelper(), zuulProperties);
}
......
......@@ -15,6 +15,8 @@
*/
package de.codecentric.boot.admin.registry;
import static java.util.Arrays.asList;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
......@@ -23,6 +25,10 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;
......@@ -30,6 +36,7 @@ import de.codecentric.boot.admin.event.ClientApplicationStatusChangedEvent;
import de.codecentric.boot.admin.model.Application;
import de.codecentric.boot.admin.model.StatusInfo;
import de.codecentric.boot.admin.registry.store.ApplicationStore;
import de.codecentric.boot.admin.web.client.HttpHeadersProvider;
/**
* The StatusUpdater is responsible for updating the status of all or a single application querying
......@@ -42,12 +49,15 @@ public class StatusUpdater implements ApplicationEventPublisherAware {
private final ApplicationStore store;
private final RestTemplate restTemplate;
private final HttpHeadersProvider httpHeadersProvider;
private ApplicationEventPublisher publisher;
private long statusLifetime = 10_000L;
public StatusUpdater(RestTemplate restTemplate, ApplicationStore store) {
public StatusUpdater(RestTemplate restTemplate, ApplicationStore store,
HttpHeadersProvider httpHeadersProvider) {
this.restTemplate = restTemplate;
this.store = store;
this.httpHeadersProvider = httpHeadersProvider;
}
public void updateStatusForAllApplications() {
......@@ -76,11 +86,7 @@ public class StatusUpdater implements ApplicationEventPublisherAware {
LOGGER.trace("Updating status for {}", application);
try {
@SuppressWarnings("unchecked")
ResponseEntity<Map<String, Serializable>> response = restTemplate.getForEntity(
application.getHealthUrl(),
(Class<Map<String, Serializable>>) (Class<?>) Map.class);
LOGGER.debug("/health for {} responded with {}", application, response);
ResponseEntity<Map<String, Serializable>> response = doGetStatus(application);
if (response.hasBody() && response.getBody().get("status") instanceof String) {
return StatusInfo.valueOf((String) response.getBody().get("status"),
......@@ -100,6 +106,22 @@ public class StatusUpdater implements ApplicationEventPublisherAware {
}
}
private ResponseEntity<Map<String, Serializable>> doGetStatus(Application application) {
@SuppressWarnings("unchecked")
Class<Map<String, Serializable>> responseType = (Class<Map<String, Serializable>>) (Class<?>) Map.class;
HttpHeaders headers = new HttpHeaders();
headers.setAccept(asList(MediaType.APPLICATION_JSON));
headers.putAll(httpHeadersProvider.getHeaders(application));
ResponseEntity<Map<String, Serializable>> response = restTemplate.exchange(
application.getHealthUrl(), HttpMethod.GET, new HttpEntity<Void>(headers),
responseType);
LOGGER.debug("/health for {} responded with {}", application, response);
return response;
}
protected Map<String, Serializable> toDetails(Exception ex) {
Map<String, Serializable> details = new HashMap<>();
details.put("message", ex.getMessage());
......
package de.codecentric.boot.admin.web.client;
import java.nio.charset.StandardCharsets;
import org.springframework.http.HttpHeaders;
import org.springframework.util.Base64Utils;
import org.springframework.util.StringUtils;
import de.codecentric.boot.admin.model.Application;
/**
* Provides Basic Auth headers for the {@link Application} using the metadata for "user.name" and
* "user.password".
*
* @author Johannes Edmeier
*/
public class BasicAuthHttpHeaderProvider implements HttpHeadersProvider {
@Override
public HttpHeaders getHeaders(Application application) {
String username = application.getMetadata().get("user.name");
String password = application.getMetadata().get("user.password");
HttpHeaders headers = new HttpHeaders();
if (StringUtils.hasText(username) && StringUtils.hasText(password)) {
headers.set(HttpHeaders.AUTHORIZATION, encode(username, password));
}
return headers;
}
protected String encode(String username, String password) {
String token = Base64Utils
.encodeToString((username + ":" + password).getBytes(StandardCharsets.UTF_8));
return "Basic " + token;
}
}
/*
* Copyright 2013-2016 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.client;
import org.springframework.http.HttpHeaders;
import de.codecentric.boot.admin.model.Application;
/**
* Is responsible to provide the {@link HttpHeaders} used to interact with the given
* {@link Application}.
*
* @author Johannes Edmeier
*/
public interface HttpHeadersProvider {
HttpHeaders getHeaders(Application application);
}
......@@ -43,7 +43,7 @@ public class ApplicationRouteLocator implements RefreshableRouteLocator {
private ApplicationRegistry registry;
private PathMatcher pathMatcher = new AntPathMatcher();
private AtomicReference<List<Route>> routes = new AtomicReference<>();
private AtomicReference<List<ApplicationRoute>> routes = new AtomicReference<>();
private String prefix;
private String servletPath;
private String[] endpoints = {};
......@@ -55,18 +55,17 @@ public class ApplicationRouteLocator implements RefreshableRouteLocator {
this.prefix = prefix;
}
protected List<Route> locateRoutes() {
protected List<ApplicationRoute> locateRoutes() {
Collection<Application> applications = registry.getApplications();
List<Route> locateRoutes = new ArrayList<>(
List<ApplicationRoute> locateRoutes = new ArrayList<>(
applications.size() * (endpoints.length + 1));
for (Application application : applications) {
addRoute(locateRoutes, application.getId(), "health", application.getHealthUrl());
addRoute(locateRoutes, application, "health", application.getHealthUrl());
if (!StringUtils.isEmpty(application.getManagementUrl())) {
for (String endpoint : endpoints) {
addRoute(locateRoutes, application.getId(), endpoint,
addRoute(locateRoutes, application, endpoint,
application.getManagementUrl() + "/" + endpoint);
}
}
......@@ -75,16 +74,16 @@ public class ApplicationRouteLocator implements RefreshableRouteLocator {
return locateRoutes;
}
private void addRoute(List<Route> locateRoutes, String applicationId, String endpoint,
String targetUrl) {
String routeId = applicationId + "-" + endpoint;
Route route = new Route(routeId, "/**", targetUrl, prefix + applicationId + "/" + endpoint,
false, null);
private void addRoute(List<ApplicationRoute> locateRoutes, Application application,
String endpoint, String targetUrl) {
ApplicationRoute route = new ApplicationRoute(application,
application.getId() + "-" + endpoint, "/**", targetUrl,
prefix + application.getId() + "/" + endpoint);
locateRoutes.add(route);
}
@Override
public Route getMatchingRoute(final String path) {
public ApplicationRoute getMatchingRoute(final String path) {
LOGGER.debug("Finding route for path: {}", path);
if (this.routes.get() == null) {
......@@ -95,7 +94,7 @@ public class ApplicationRouteLocator implements RefreshableRouteLocator {
String adjustedPath = stripServletPath(path);
for (Route route : this.routes.get()) {
for (ApplicationRoute route : this.routes.get()) {
String pattern = route.getFullPath();
LOGGER.debug("Matching pattern: {}", pattern);
if (this.pathMatcher.match(pattern, adjustedPath)) {
......@@ -107,15 +106,15 @@ public class ApplicationRouteLocator implements RefreshableRouteLocator {
return null;
}
private Route adjustPathRoute(Route route, String path) {
private ApplicationRoute adjustPathRoute(ApplicationRoute route, String path) {
String adjustedPath;
if (path.startsWith(route.getPrefix())) {
adjustedPath = path.substring(route.getPrefix().length());
} else {
adjustedPath = path;
}
return new Route(route.getId(), adjustedPath, route.getLocation(), route.getPrefix(),
route.getRetryable(), null);
return new ApplicationRoute(route.getApplication(), route.getId(), adjustedPath,
route.getLocation(), route.getPrefix());
}
@Override
......@@ -123,7 +122,7 @@ public class ApplicationRouteLocator implements RefreshableRouteLocator {
if (this.routes.get() == null) {
this.routes.set(locateRoutes());
}
return new ArrayList<>(routes.get());
return new ArrayList<Route>(routes.get());
}
@Override
......@@ -148,10 +147,25 @@ public class ApplicationRouteLocator implements RefreshableRouteLocator {
String adjustedPath = path;
if (StringUtils.hasText(servletPath) && !"/".equals(servletPath)) {
adjustedPath = path.substring(this.servletPath.length());
adjustedPath = path.substring(this.servletPath.length());
}
LOGGER.debug("adjustedPath={}", path);
return adjustedPath;
}
public static class ApplicationRoute extends Route {
private final Application application;
public ApplicationRoute(Application application, String id, String path, String location,
String prefix) {
super(id, path, location, prefix, false, null);
this.application = application;
}
public Application getApplication() {
return application;
}
}
}
/*
* 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 de.codecentric.boot.admin.zuul.filters.pre;
import java.util.List;
import java.util.Map.Entry;
import org.springframework.http.HttpHeaders;
import org.springframework.web.util.UrlPathHelper;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import de.codecentric.boot.admin.web.client.HttpHeadersProvider;
import de.codecentric.boot.admin.zuul.ApplicationRouteLocator;
import de.codecentric.boot.admin.zuul.ApplicationRouteLocator.ApplicationRoute;
/**
* This filter adds headers to the zuulRequest specific for the application using a
* {@link HttpHeadersProvider}.
*
* @author Johannes Edmeier
*/
public class ApplicationHeadersFilter extends ZuulFilter {
private final HttpHeadersProvider headersProvider;
private final ApplicationRouteLocator routeLocator;
private final UrlPathHelper urlPathHelper = new UrlPathHelper();
public ApplicationHeadersFilter(HttpHeadersProvider headersProvider,
ApplicationRouteLocator routeLocator) {
this.headersProvider = headersProvider;
this.routeLocator = routeLocator;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public int filterOrder() {
return 0;
}
@Override
public String filterType() {
return "pre";
}
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
String requestURI = this.urlPathHelper.getPathWithinApplication(ctx.getRequest());
ApplicationRoute route = this.routeLocator.getMatchingRoute(requestURI);
if (route != null) {
HttpHeaders headers = headersProvider.getHeaders(route.getApplication());
for (Entry<String, List<String>> header : headers.entrySet()) {
ctx.addZuulRequestHeader(header.getKey(), header.getValue().get(0));
}
}
return null;
}
}
\ No newline at end of file
......@@ -15,10 +15,11 @@
*/
package de.codecentric.boot.admin.registry;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.isA;
import static org.junit.Assert.assertThat;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.argThat;
import static org.mockito.Matchers.eq;
import static org.mockito.Matchers.isA;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
......@@ -27,9 +28,13 @@ import static org.mockito.Mockito.when;
import java.util.Collections;
import java.util.Map;
import org.hamcrest.CoreMatchers;
import org.junit.Before;
import org.junit.Test;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.ResourceAccessException;
import org.springframework.web.client.RestTemplate;
......@@ -38,6 +43,7 @@ import de.codecentric.boot.admin.event.ClientApplicationStatusChangedEvent;
import de.codecentric.boot.admin.model.Application;
import de.codecentric.boot.admin.model.StatusInfo;
import de.codecentric.boot.admin.registry.store.SimpleApplicationStore;
import de.codecentric.boot.admin.web.client.HttpHeadersProvider;
public class StatusUpdaterTest {
......@@ -48,9 +54,12 @@ public class StatusUpdaterTest {
@Before
public void setup() {
HttpHeadersProvider httpHeadersProvider = mock(HttpHeadersProvider.class);
when(httpHeadersProvider.getHeaders(any(Application.class))).thenReturn(new HttpHeaders());
store = new SimpleApplicationStore();
template = mock(RestTemplate.class);
updater = new StatusUpdater(template, store);
updater = new StatusUpdater(template, store, httpHeadersProvider);
publisher = mock(ApplicationEventPublisher.class);
updater.setApplicationEventPublisher(publisher);
}
......@@ -58,80 +67,84 @@ public class StatusUpdaterTest {
@Test
@SuppressWarnings("rawtypes")
public void test_update_statusChanged() {
when(template.getForEntity("health", Map.class)).thenReturn(
ResponseEntity.ok().body((Map) Collections.singletonMap("status", "UP")));
when(template.exchange(eq("health"), eq(HttpMethod.GET), isA(HttpEntity.class),
eq(Map.class))).thenReturn(
ResponseEntity.ok().body((Map) Collections.singletonMap("status", "UP")));
updater.updateStatus(
Application.create("foo").withId("id").withHealthUrl("health").build());
Application app = store.find("id");
assertThat(app.getStatusInfo().getStatus(), is("UP"));
verify(publisher).publishEvent(argThat(isA(ClientApplicationStatusChangedEvent.class)));
assertThat(app.getStatusInfo().getStatus(), CoreMatchers.is("UP"));
verify(publisher)
.publishEvent(argThat(CoreMatchers.isA(ClientApplicationStatusChangedEvent.class)));
}
@Test
@SuppressWarnings("rawtypes")
public void test_update_statusUnchanged() {
when(template.getForEntity("health", Map.class))
.thenReturn(ResponseEntity.ok((Map) Collections.singletonMap("status", "UNKNOWN")));
when(template.exchange(eq("health"), eq(HttpMethod.GET), isA(HttpEntity.class),
eq(Map.class))).thenReturn(
ResponseEntity.ok((Map) Collections.singletonMap("status", "UNKNOWN")));
updater.updateStatus(
Application.create("foo").withId("id").withHealthUrl("health").build());
verify(publisher, never())
.publishEvent(argThat(isA(ClientApplicationStatusChangedEvent.class)));
.publishEvent(argThat(CoreMatchers.isA(ClientApplicationStatusChangedEvent.class)));
}
@Test
@SuppressWarnings("rawtypes")
public void test_update_noBody() {
// HTTP 200 - UP
when(template.getForEntity("health", Map.class)).thenReturn(ResponseEntity.ok((Map) null));
when(template.exchange(eq("health"), eq(HttpMethod.GET), isA(HttpEntity.class),
eq(Map.class))).thenReturn(ResponseEntity.ok((Map) null));
updater.updateStatus(
Application.create("foo").withId("id").withHealthUrl("health").build());
assertThat(store.find("id").getStatusInfo().getStatus(), is("UP"));
assertThat(store.find("id").getStatusInfo().getStatus(), CoreMatchers.is("UP"));
// HTTP != 200 - DOWN
when(template.getForEntity("health", Map.class))
.thenReturn(ResponseEntity.status(503).body((Map) null));
when(template.exchange(eq("health"), eq(HttpMethod.GET), isA(HttpEntity.class),
eq(Map.class))).thenReturn(ResponseEntity.status(503).body((Map) null));
updater.updateStatus(
Application.create("foo").withId("id").withHealthUrl("health").build());
assertThat(store.find("id").getStatusInfo().getStatus(), is("DOWN"));
assertThat(store.find("id").getStatusInfo().getStatus(), CoreMatchers.is("DOWN"));
}
@Test
public void test_update_offline() {
when(template.getForEntity("health", Map.class))
.thenThrow(new ResourceAccessException("error"));
when(template.exchange(eq("health"), eq(HttpMethod.GET), isA(HttpEntity.class),
eq(Map.class))).thenThrow(new ResourceAccessException("error"));
updater.updateStatus(
Application.create("foo").withId("id").withHealthUrl("health").build());
Application app = Application.create("foo").withId("id").withHealthUrl("health")
.withStatusInfo(StatusInfo.ofUp()).build();
updater.updateStatus(app);
assertThat(store.find("id").getStatusInfo().getStatus(), is("OFFLINE"));
assertThat(store.find("id").getStatusInfo().getStatus(), CoreMatchers.is("OFFLINE"));
}
@Test
@SuppressWarnings("rawtypes")
public void test_updateStatusForApplications() {
Application app1 = Application.create("foo").withId("id-1").withHealthUrl("health-1")
.build();
store.save(app1);
Application app2 = Application.create("foo").withId("id-2").withHealthUrl("health-2")
.withStatusInfo(StatusInfo.ofUp()).build();
store.save(app2);
public void test_updateStatusForApplications() throws InterruptedException {
updater.setStatusLifetime(100L);
store.save(Application.create("foo").withId("id-1").withHealthUrl("health-1").build());
Thread.sleep(120L); // Let the StatusInfo of id-1 expire
store.save(Application.create("foo").withId("id-2").withHealthUrl("health-2").build());
when(template.getForEntity("health-2", Map.class))
.thenReturn(ResponseEntity.ok((Map) null));
when(template.exchange(eq("health-1"), eq(HttpMethod.GET), isA(HttpEntity.class),
eq(Map.class))).thenReturn(ResponseEntity.ok((Map) null));
updater.updateStatusForAllApplications();
verify(template, never()).getForEntity("health-1", Map.class);
assertThat(store.find("id-1").getStatusInfo().getStatus(), CoreMatchers.is("UP"));
verify(template, never()).exchange(eq("health-2"), eq(HttpMethod.GET),
isA(HttpEntity.class), eq(Map.class));
}
}
package de.codecentric.boot.admin.web.client;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import org.junit.Test;
import com.google.common.net.HttpHeaders;
import de.codecentric.boot.admin.model.Application;
public class BasicAuthHttpHeaderProviderTest {
private BasicAuthHttpHeaderProvider headersProvider = new BasicAuthHttpHeaderProvider();
@Test
public void test_auth_header() {
Application app = Application.create("test").withHealthUrl("/health")
.withMetadata("user.name", "test").withMetadata("user.password", "drowssap")
.build();
assertThat(headersProvider.getHeaders(app).get(HttpHeaders.AUTHORIZATION).get(0),
is("Basic dGVzdDpkcm93c3NhcA=="));
}
@Test
public void test_no_header() {
Application app = Application.create("test").withHealthUrl("/health").build();
assertTrue(headersProvider.getHeaders(app).isEmpty());
}
}
package de.codecentric.boot.admin.zuul.filters.pre;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import org.junit.Test;
import org.springframework.http.HttpHeaders;
import org.springframework.mock.web.MockHttpServletRequest;
import com.netflix.zuul.context.RequestContext;
import de.codecentric.boot.admin.model.Application;
import de.codecentric.boot.admin.web.client.HttpHeadersProvider;
import de.codecentric.boot.admin.zuul.ApplicationRouteLocator;
import de.codecentric.boot.admin.zuul.ApplicationRouteLocator.ApplicationRoute;
public class ApplicationHeadersFilterTest {
private ApplicationRouteLocator routeLocator = mock(ApplicationRouteLocator.class);
private HttpHeadersProvider headerProvider = mock(HttpHeadersProvider.class);
private ApplicationHeadersFilter filter = new ApplicationHeadersFilter(headerProvider,
routeLocator);
@Test
public void test_add_headers_on_matching_route_only() {
Application application = Application.create("test").withHealthUrl("/health").build();
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.add("test", "qwertz");
when(routeLocator.getMatchingRoute("/health"))
.thenReturn(new ApplicationRoute(application, null, null, null, null));
when(headerProvider.getHeaders(application)).thenReturn(httpHeaders);
RequestContext context = creteRequestContext("/health");
RequestContext.testSetCurrentContext(context);
filter.run();
assertThat(context.getZuulRequestHeaders().get("test"), is("qwertz"));
context = creteRequestContext("/foobar");
RequestContext.testSetCurrentContext(context);
filter.run();
assertThat(context.getZuulRequestHeaders().get("test"), nullValue());
}
private RequestContext creteRequestContext(String uri) {
RequestContext context = new RequestContext();
MockHttpServletRequest request = new MockHttpServletRequest();
request.setRequestURI(uri);
context.setRequest(request);
return context;
}
}
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