Introduce management base-path property for servlet and reactive actuator

Previously, the base path of a servlet-based management server could be
configured using management.server.servlet.context-path but there was no
equivalent property for WebFlux.

This commit introduces a new property, management.server.base-path,
that can be used with both servlet and reactive management servers. The
existing servlet-specific property has been deprecated in favour of the
new general property. When using the servlet stack, if both the general
property and the servlet-specific property are set, the new general
property takes precedence. When using the reactive stack, only the new
general property is considered.

Closes gh-22906
This commit is contained in:
Andy Wilkinson
2020-10-26 10:21:47 +00:00
parent 653e64c4ef
commit 10f887a5ad
11 changed files with 258 additions and 21 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2020 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.
@@ -38,8 +38,11 @@ public class WebEndpointProperties {
private final Exposure exposure = new Exposure();
/**
* Base path for Web endpoints. Relative to server.servlet.context-path or
* management.server.servlet.context-path if management.server.port is configured.
* Base path for Web endpoints. Relative to the servlet context path
* (server.servlet.context-path) or WebFlux base path (spring.webflux.base-path) when
* the management server is sharing the main server port. Relative to the management
* server base path (management.server.base-path) when a separate management server
* port (management.server.port) is configured.
*/
private String basePath = "/actuator";

View File

@@ -16,9 +16,13 @@
package org.springframework.boot.actuate.autoconfigure.web.reactive;
import java.util.Collections;
import java.util.Map;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration;
import org.springframework.boot.actuate.autoconfigure.web.ManagementContextType;
import org.springframework.boot.actuate.autoconfigure.web.server.ManagementServerProperties;
import org.springframework.boot.actuate.autoconfigure.web.server.ManagementWebServerFactoryCustomizer;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
@@ -31,7 +35,9 @@ import org.springframework.boot.autoconfigure.web.reactive.TomcatReactiveWebServ
import org.springframework.boot.web.reactive.server.ConfigurableReactiveWebServerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.http.server.reactive.ContextPathCompositeHandler;
import org.springframework.http.server.reactive.HttpHandler;
import org.springframework.util.StringUtils;
import org.springframework.web.reactive.config.EnableWebFlux;
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
@@ -56,8 +62,13 @@ public class ReactiveManagementChildContextConfiguration {
}
@Bean
public HttpHandler httpHandler(ApplicationContext applicationContext) {
return WebHttpHandlerBuilder.applicationContext(applicationContext).build();
public HttpHandler httpHandler(ApplicationContext applicationContext, ManagementServerProperties properties) {
HttpHandler httpHandler = WebHttpHandlerBuilder.applicationContext(applicationContext).build();
if (StringUtils.hasText(properties.getBasePath())) {
Map<String, HttpHandler> handlersMap = Collections.singletonMap(properties.getBasePath(), httpHandler);
return new ContextPathCompositeHandler(handlersMap);
}
return httpHandler;
}
static class ReactiveManagementWebServerFactoryCustomizer

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2020 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.
@@ -20,6 +20,7 @@ import java.net.InetAddress;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.DeprecatedConfigurationProperty;
import org.springframework.boot.context.properties.NestedConfigurationProperty;
import org.springframework.boot.web.server.Ssl;
import org.springframework.util.Assert;
@@ -49,6 +50,12 @@ public class ManagementServerProperties {
*/
private InetAddress address;
/**
* Management endpoint base path (for instance, `/management`). Requires a custom
* management.server.port.
*/
private String basePath = "";
private final Servlet servlet = new Servlet();
@NestedConfigurationProperty
@@ -82,6 +89,14 @@ public class ManagementServerProperties {
this.address = address;
}
public String getBasePath() {
return this.basePath;
}
public void setBasePath(String basePath) {
this.basePath = cleanBasePath(basePath);
}
public Ssl getSsl() {
return this.ssl;
}
@@ -94,6 +109,19 @@ public class ManagementServerProperties {
return this.servlet;
}
private String cleanBasePath(String basePath) {
String candidate = StringUtils.trimWhitespace(basePath);
if (StringUtils.hasText(candidate)) {
if (!candidate.startsWith("/")) {
candidate = "/" + candidate;
}
if (candidate.endsWith("/")) {
candidate = candidate.substring(0, candidate.length() - 1);
}
}
return candidate;
}
/**
* Servlet properties.
*/
@@ -109,11 +137,22 @@ public class ManagementServerProperties {
* Return the context path with no trailing slash (i.e. the '/' root context is
* represented as the empty string).
* @return the context path (no trailing slash)
* @deprecated since 2.4.0 in favor of
* {@link ManagementServerProperties#getBasePath()}
*/
@Deprecated
@DeprecatedConfigurationProperty(replacement = "management.server.base-path")
public String getContextPath() {
return this.contextPath;
}
/**
* Set the context path.
* @param contextPath the context path
* @deprecated since 2.4.0 in favor of
* {@link ManagementServerProperties#setBasePath(String)}
*/
@Deprecated
public void setContextPath(String contextPath) {
Assert.notNull(contextPath, "ContextPath must not be null");
this.contextPath = cleanContextPath(contextPath);

View File

@@ -123,7 +123,13 @@ class ServletManagementChildContextConfiguration {
protected void customize(ConfigurableServletWebServerFactory webServerFactory,
ManagementServerProperties managementServerProperties, ServerProperties serverProperties) {
super.customize(webServerFactory, managementServerProperties, serverProperties);
webServerFactory.setContextPath(managementServerProperties.getServlet().getContextPath());
webServerFactory.setContextPath(getContextPath(managementServerProperties));
}
@SuppressWarnings("deprecation")
private String getContextPath(ManagementServerProperties managementServerProperties) {
String basePath = managementServerProperties.getBasePath();
return StringUtils.hasText(basePath) ? basePath : managementServerProperties.getServlet().getContextPath();
}
}

View File

@@ -0,0 +1,97 @@
/*
* Copyright 2012-2020 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
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.autoconfigure.web.reactive;
import java.util.function.Consumer;
import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.web.server.ManagementContextAutoConfiguration;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.web.reactive.HttpHandlerAutoConfiguration;
import org.springframework.boot.autoconfigure.web.reactive.ReactiveWebServerFactoryAutoConfiguration;
import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration;
import org.springframework.boot.test.context.assertj.AssertableReactiveWebApplicationContext;
import org.springframework.boot.test.context.runner.ContextConsumer;
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
import org.springframework.boot.web.context.ServerPortInfoApplicationContextInitializer;
import org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.client.WebClient;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests for {@link ReactiveManagementChildContextConfiguration}.
*
* @author Andy Wilkinson
*/
class ReactiveManagementChildContextConfigurationIntegrationTests {
private final ReactiveWebApplicationContextRunner runner = new ReactiveWebApplicationContextRunner(
AnnotationConfigReactiveWebServerApplicationContext::new)
.withConfiguration(AutoConfigurations.of(ManagementContextAutoConfiguration.class,
ReactiveWebServerFactoryAutoConfiguration.class,
ReactiveManagementContextAutoConfiguration.class, WebEndpointAutoConfiguration.class,
EndpointAutoConfiguration.class, HttpHandlerAutoConfiguration.class,
WebFluxAutoConfiguration.class))
.withUserConfiguration(SucceedingEndpoint.class)
.withInitializer(new ServerPortInfoApplicationContextInitializer()).withPropertyValues(
"server.port=0", "management.server.port=0", "management.endpoints.web.exposure.include=*");
@Test
void endpointsAreBeneathActuatorByDefault() {
this.runner.withPropertyValues("management.server.port:0").run(withWebTestClient((client) -> {
String body = client.get().uri("actuator/success").accept(MediaType.APPLICATION_JSON)
.exchangeToMono((response) -> response.bodyToMono(String.class)).block();
assertThat(body).isEqualTo("Success");
}));
}
@Test
void whenManagementServerBasePathIsConfiguredThenEndpointsAreBeneathThatPath() {
this.runner.withPropertyValues("management.server.port:0", "management.server.base-path:/manage")
.run(withWebTestClient((client) -> {
String body = client.get().uri("manage/actuator/success").accept(MediaType.APPLICATION_JSON)
.exchangeToMono((response) -> response.bodyToMono(String.class)).block();
assertThat(body).isEqualTo("Success");
}));
}
private ContextConsumer<AssertableReactiveWebApplicationContext> withWebTestClient(Consumer<WebClient> webClient) {
return (context) -> {
String port = context.getEnvironment().getProperty("local.management.port");
WebClient client = WebClient.create("http://localhost:" + port);
webClient.accept(client);
};
}
@Endpoint(id = "success")
static class SucceedingEndpoint {
@ReadOperation
String fail() {
return "Success";
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2020 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.
@@ -29,22 +29,48 @@ import static org.assertj.core.api.Assertions.assertThat;
class ManagementServerPropertiesTests {
@Test
void defaultManagementServerProperties() {
void defaultPortIsNull() {
ManagementServerProperties properties = new ManagementServerProperties();
assertThat(properties.getPort()).isNull();
}
@Test
void definedPort() {
ManagementServerProperties properties = new ManagementServerProperties();
properties.setPort(123);
assertThat(properties.getPort()).isEqualTo(123);
}
@Test
@Deprecated
void defaultContextPathIsEmptyString() {
ManagementServerProperties properties = new ManagementServerProperties();
assertThat(properties.getServlet().getContextPath()).isEqualTo("");
}
@Test
void definedManagementServerProperties() {
@Deprecated
void definedContextPath() {
ManagementServerProperties properties = new ManagementServerProperties();
properties.setPort(123);
properties.getServlet().setContextPath("/foo");
assertThat(properties.getPort()).isEqualTo(123);
assertThat(properties.getServlet().getContextPath()).isEqualTo("/foo");
}
@Test
void defaultBasePathIsEmptyString() {
ManagementServerProperties properties = new ManagementServerProperties();
assertThat(properties.getBasePath()).isEqualTo("");
}
@Test
void definedBasePath() {
ManagementServerProperties properties = new ManagementServerProperties();
properties.setBasePath("/foo");
assertThat(properties.getBasePath()).isEqualTo("/foo");
}
@Test
@Deprecated
void trailingSlashOfContextPathIsRemoved() {
ManagementServerProperties properties = new ManagementServerProperties();
properties.getServlet().setContextPath("/foo/");
@@ -52,10 +78,25 @@ class ManagementServerPropertiesTests {
}
@Test
void trailingSlashOfBasePathIsRemoved() {
ManagementServerProperties properties = new ManagementServerProperties();
properties.setBasePath("/foo/");
assertThat(properties.getBasePath()).isEqualTo("/foo");
}
@Test
@Deprecated
void slashOfContextPathIsDefaultValue() {
ManagementServerProperties properties = new ManagementServerProperties();
properties.getServlet().setContextPath("/");
assertThat(properties.getServlet().getContextPath()).isEqualTo("");
}
@Test
void slashOfBasePathIsDefaultValue() {
ManagementServerProperties properties = new ManagementServerProperties();
properties.setBasePath("/");
assertThat(properties.getBasePath()).isEqualTo("");
}
}

View File

@@ -68,7 +68,8 @@ class WebMvcEndpointChildContextConfigurationIntegrationTests {
ServletManagementContextAutoConfiguration.class, WebEndpointAutoConfiguration.class,
EndpointAutoConfiguration.class, DispatcherServletAutoConfiguration.class,
ErrorMvcAutoConfiguration.class))
.withUserConfiguration(FailingEndpoint.class, FailingControllerEndpoint.class)
.withUserConfiguration(SucceedingEndpoint.class, FailingEndpoint.class,
FailingControllerEndpoint.class)
.withInitializer(new ServerPortInfoApplicationContextInitializer())
.withPropertyValues("server.port=0", "management.server.port=0",
"management.endpoints.web.exposure.include=*", "server.error.include-exception=true",
@@ -125,6 +126,35 @@ class WebMvcEndpointChildContextConfigurationIntegrationTests {
}));
}
@Test
void whenManagementServerBasePathIsConfiguredThenEndpointsAreBeneathThatPath() {
this.runner.withPropertyValues("management.server.base-path:/manage").run(withWebTestClient((client) -> {
String body = client.get().uri("manage/actuator/success").accept(MediaType.APPLICATION_JSON)
.exchangeToMono((response) -> response.bodyToMono(String.class)).block();
assertThat(body).isEqualTo("Success");
}));
}
@Test
void whenManagementServletContextPathIsConfiguredThenEndpointsAreBeneathThatPath() {
this.runner.withPropertyValues("management.server.servlet.context-path:/manage")
.run(withWebTestClient((client) -> {
String body = client.get().uri("manage/actuator/success").accept(MediaType.APPLICATION_JSON)
.exchangeToMono((response) -> response.bodyToMono(String.class)).block();
assertThat(body).isEqualTo("Success");
}));
}
@Test
void whenManagementBasePathAndServletContextPathAreConfiguredThenEndpointsAreBeneathBasePath() {
this.runner.withPropertyValues("management.server.servlet.context-path:/admin",
"management.server.base-path:/manage").run(withWebTestClient((client) -> {
String body = client.get().uri("manage/actuator/success").accept(MediaType.APPLICATION_JSON)
.exchangeToMono((response) -> response.bodyToMono(String.class)).block();
assertThat(body).isEqualTo("Success");
}));
}
private ContextConsumer<AssertableWebApplicationContext> withWebTestClient(Consumer<WebClient> webClient) {
return (context) -> {
String port = context.getEnvironment().getProperty("local.management.port");
@@ -148,6 +178,16 @@ class WebMvcEndpointChildContextConfigurationIntegrationTests {
}
@Endpoint(id = "success")
static class SucceedingEndpoint {
@ReadOperation
String fail() {
return "Success";
}
}
@RestControllerEndpoint(id = "failController")
static class FailingControllerEndpoint {

View File

@@ -1272,8 +1272,8 @@ You can use the configprop:management.endpoints.web.base-path[] property to chan
The preceding `application.properties` example changes the endpoint from `/actuator/\{id}` to `/manage/\{id}` (for example, `/manage/info`).
NOTE: Unless the management port has been configured to <<production-ready-customizing-management-server-port,expose endpoints by using a different HTTP port>>, `management.endpoints.web.base-path` is relative to `server.servlet.context-path`.
If `management.server.port` is configured, `management.endpoints.web.base-path` is relative to `management.server.servlet.context-path`.
NOTE: Unless the management port has been configured to <<production-ready-customizing-management-server-port,expose endpoints by using a different HTTP port>>, `management.endpoints.web.base-path` is relative to `server.servlet.context-path` (Servlet web applications) or `spring.webflux.base-path` (reactive web applications).
If `management.server.port` is configured, `management.endpoints.web.base-path` is relative to `management.server.base-path`.
If you want to map endpoints to a different path, you can use the configprop:management.endpoints.web.path-mapping[] property.

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2020 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.
@@ -38,7 +38,7 @@ import static org.assertj.core.api.Assertions.assertThat;
* @author Madhura Bhave
*/
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT,
properties = { "management.server.port=0", "management.server.servlet.context-path=/management" })
properties = { "management.server.port=0", "management.server.base-path=/management" })
class ManagementPortAndPathSampleActuatorApplicationTests extends AbstractSampleActuatorCustomSecurityTests {
@LocalServerPort

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2020 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.
@@ -36,7 +36,7 @@ import static org.assertj.core.api.Assertions.assertThat;
* @author Dave Syer
*/
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = { "management.server.port=0",
"management.server.address=127.0.0.1", "management.server.servlet.context-path:/admin" })
"management.server.address=127.0.0.1", "management.server.base-path:/admin" })
class ManagementAddressActuatorApplicationTests {
@LocalServerPort

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2020 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.
@@ -36,7 +36,7 @@ import static org.assertj.core.api.Assertions.assertThat;
* @author Madhura Bhave
*/
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT,
properties = { "management.server.port=0", "management.server.servlet.context-path=/management" })
properties = { "management.server.port=0", "management.server.base-path=/management" })
class ManagementPortAndPathJerseyApplicationTests extends AbstractJerseySecureTests {
@LocalServerPort