Add CloudFoundry EndpointHandlerMapping

Add a CloudFoundryEndpointHandlerMapping that can expose actuator
endpoints for Cloud Foundry "appsmanager" to use.

See gh-7108
This commit is contained in:
Madhura Bhave 2016-10-11 15:04:11 -07:00 committed by Phillip Webb
parent 570b292df7
commit 7afb161fcf
10 changed files with 388 additions and 34 deletions

View File

@ -406,7 +406,8 @@ public class ManagementWebSecurityAutoConfiguration {
EndpointHandlerMapping endpointHandlerMapping = null;
ApplicationContext context = this.contextResolver.getApplicationContext();
if (context.getBeanNamesForType(EndpointHandlerMapping.class).length > 0) {
endpointHandlerMapping = context.getBean(EndpointHandlerMapping.class);
endpointHandlerMapping = context.getBean("endpointHandlerMapping",
EndpointHandlerMapping.class);
}
if (endpointHandlerMapping == null) {
// Maybe there are actually no endpoints (e.g. management.port=-1)

View File

@ -0,0 +1,66 @@
/*
* Copyright 2012-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 org.springframework.boot.actuate.cloudfoundry;
import java.util.LinkedHashSet;
import java.util.Set;
import org.springframework.boot.actuate.autoconfigure.EndpointWebMvcAutoConfiguration;
import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoints;
import org.springframework.boot.actuate.endpoint.mvc.NamedMvcEndpoint;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnCloudPlatform;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.cloud.CloudPlatform;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
/**
* {@link EnableAutoConfiguration Auto-configuration} to expose actuator endpoints for
* cloud foundry to use.
*
* @author Madhura Bhave
* @since 1.5.0
*/
@Configuration
@ConditionalOnProperty(prefix = "management.cloudfoundry", name = "enabled", matchIfMissing = false)
@ConditionalOnBean(MvcEndpoints.class)
@AutoConfigureAfter(EndpointWebMvcAutoConfiguration.class)
@ConditionalOnCloudPlatform(CloudPlatform.CLOUD_FOUNDRY)
public class CloudFoundryActuatorAutoConfiguration {
@Bean
public CloudFoundryEndpointHandlerMapping cloudFoundryEndpointHandlerMapping(
MvcEndpoints mvcEndpoints) {
Set<NamedMvcEndpoint> endpoints = new LinkedHashSet<NamedMvcEndpoint>(
mvcEndpoints.getEndpoints(NamedMvcEndpoint.class));
CloudFoundryEndpointHandlerMapping mapping = new CloudFoundryEndpointHandlerMapping(
endpoints, getCorsConfiguration());
mapping.setPrefix("/cloudfoundryapplication");
return mapping;
}
private CorsConfiguration getCorsConfiguration() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.addAllowedOrigin(CorsConfiguration.ALL);
return corsConfiguration;
}
}

View File

@ -0,0 +1,93 @@
/*
* Copyright 2012-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 org.springframework.boot.actuate.cloudfoundry;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.boot.actuate.endpoint.Endpoint;
import org.springframework.boot.actuate.endpoint.mvc.EndpointHandlerMapping;
import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoint;
import org.springframework.boot.actuate.endpoint.mvc.NamedMvcEndpoint;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.servlet.HandlerExecutionChain;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
/**
* {@link HandlerMapping} to map {@link Endpoint}s to Cloud Foundry specific URLs.
*
* @author Madhura Bhave
*/
class CloudFoundryEndpointHandlerMapping extends EndpointHandlerMapping {
CloudFoundryEndpointHandlerMapping(Collection<? extends NamedMvcEndpoint> endpoints) {
super(endpoints);
}
CloudFoundryEndpointHandlerMapping(Set<NamedMvcEndpoint> endpoints,
CorsConfiguration corsConfiguration) {
super(endpoints, corsConfiguration);
}
@Override
protected String getPath(MvcEndpoint endpoint) {
if (endpoint instanceof NamedMvcEndpoint) {
return "/" + ((NamedMvcEndpoint) endpoint).getName();
}
return super.getPath(endpoint);
}
@Override
protected HandlerExecutionChain getHandlerExecutionChain(Object handler,
HttpServletRequest request) {
HandlerExecutionChain chain = super.getHandlerExecutionChain(handler, request);
HandlerInterceptor[] interceptors = addSecurityInterceptor(
chain.getInterceptors());
return new HandlerExecutionChain(chain.getHandler(), interceptors);
}
private HandlerInterceptor[] addSecurityInterceptor(HandlerInterceptor[] existing) {
List<HandlerInterceptor> interceptors = new ArrayList<HandlerInterceptor>();
interceptors.add(new SecurityInterceptor());
if (existing != null) {
interceptors.addAll(Arrays.asList(existing));
}
return interceptors.toArray(new HandlerInterceptor[interceptors.size()]);
}
/**
* Security interceptor to check cloud foundry token.
*/
static class SecurityInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
// Currently open
return true;
}
}
}

View File

@ -209,29 +209,9 @@ public class EndpointHandlerMapping extends RequestMappingHandlerMapping {
/**
* Return the endpoints.
* @return the endpoints
* @see #getEndpoints(Class)
*/
public Set<? extends MvcEndpoint> getEndpoints() {
return getEndpoints(MvcEndpoint.class);
}
/**
* Return the endpoints of the specified type.
* @param <E> the endpoint type
* @param type the endpoint type
* @return the endpoints
* @see #getEndpoints()
* @since 1.5.0
*/
@SuppressWarnings("unchecked")
public <E extends MvcEndpoint> Set<E> getEndpoints(Class<E> type) {
Set<E> result = new HashSet<E>(this.endpoints.size());
for (MvcEndpoint candidate : this.endpoints) {
if (type.isInstance(candidate)) {
result.add((E) candidate);
}
}
return Collections.unmodifiableSet(result);
public Set<MvcEndpoint> getEndpoints() {
return Collections.unmodifiableSet(this.endpoints);
}
@Override

View File

@ -17,6 +17,7 @@
package org.springframework.boot.actuate.endpoint.mvc;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
@ -92,6 +93,22 @@ public class MvcEndpoints implements ApplicationContextAware, InitializingBean {
return this.endpoints;
}
/**
* Return the endpoints of the specified type.
* @param <E> the Class type of the endpoints to be returned
* @param type the endpoint type
* @return the endpoints
*/
public <E extends MvcEndpoint> Set<E> getEndpoints(Class<E> type) {
Set<E> result = new HashSet<E>(this.endpoints.size());
for (MvcEndpoint candidate: this.endpoints) {
if (type.isInstance(candidate)) {
result.add((E) candidate);
}
}
return Collections.unmodifiableSet(result);
}
private boolean isGenericEndpoint(Class<?> type) {
return !this.customTypes.contains(type)
&& !MvcEndpoint.class.isAssignableFrom(type);

View File

@ -17,7 +17,8 @@ org.springframework.boot.actuate.autoconfigure.MetricsChannelAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.MetricExportAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.PublicMetricsAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.TraceRepositoryAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.TraceWebFilterAutoConfiguration
org.springframework.boot.actuate.autoconfigure.TraceWebFilterAutoConfiguration,\
org.springframework.boot.actuate.cloudfoundry.CloudFoundryActuatorAutoConfiguration
org.springframework.boot.actuate.autoconfigure.ManagementContextConfiguration=\
org.springframework.boot.actuate.autoconfigure.EndpointWebMvcManagementContextConfiguration,\

View File

@ -0,0 +1,98 @@
/*
* Copyright 2012-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 org.springframework.boot.actuate.cloudfoundry;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.boot.actuate.autoconfigure.EndpointAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.EndpointWebMvcAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.EndpointWebMvcManagementContextConfiguration;
import org.springframework.boot.actuate.autoconfigure.ManagementServerPropertiesAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.ManagementWebSecurityAutoConfiguration;
import org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
import org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration;
import org.springframework.boot.autoconfigure.web.HttpMessageConvertersAutoConfiguration;
import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration;
import org.springframework.boot.test.util.EnvironmentTestUtils;
import org.springframework.mock.web.MockServletContext;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link CloudFoundryActuatorAutoConfiguration}.
*
* @author Madhura Bhave
*/
public class CloudFoundryActuatorAutoConfigurationTests {
private AnnotationConfigWebApplicationContext context;
@Before
public void setUp() {
this.context = new AnnotationConfigWebApplicationContext();
this.context.setServletContext(new MockServletContext());
this.context.register(SecurityAutoConfiguration.class,
WebMvcAutoConfiguration.class,
ManagementWebSecurityAutoConfiguration.class,
JacksonAutoConfiguration.class,
HttpMessageConvertersAutoConfiguration.class,
EndpointAutoConfiguration.class, EndpointWebMvcAutoConfiguration.class,
ManagementServerPropertiesAutoConfiguration.class,
PropertyPlaceholderAutoConfiguration.class,
EndpointWebMvcManagementContextConfiguration.class,
CloudFoundryActuatorAutoConfiguration.class);
}
@After
public void close() {
if (this.context != null) {
this.context.close();
}
}
@Test
public void cloudFoundryPlatformActive() throws Exception {
EnvironmentTestUtils.addEnvironment(this.context, "VCAP_APPLICATION:---",
"management.cloudfoundry.enabled:true");
this.context.refresh();
CloudFoundryEndpointHandlerMapping handlerMapping = this.context.getBean(
"cloudFoundryEndpointHandlerMapping",
CloudFoundryEndpointHandlerMapping.class);
assertThat(handlerMapping.getPrefix()).isEqualTo("/cloudfoundryapplication");
}
@Test
public void cloudFoundryPlatformInactive() throws Exception {
this.context.refresh();
assertThat(this.context.containsBean("cloudFoundryEndpointHandlerMapping"))
.isFalse();
}
@Test
public void cloudFoundryManagementEndpointsDisabled() throws Exception {
EnvironmentTestUtils.addEnvironment(this.context, "VCAP_APPLICATION=---",
"management.cloudfoundry.enabled:false");
this.context.refresh();
assertThat(this.context.containsBean("cloudFoundryEndpointHandlerMapping"))
.isFalse();
}
}

View File

@ -0,0 +1,81 @@
/*
* Copyright 2012-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 org.springframework.boot.actuate.cloudfoundry;
import java.util.Arrays;
import org.junit.Test;
import org.springframework.boot.actuate.endpoint.AbstractEndpoint;
import org.springframework.boot.actuate.endpoint.mvc.EndpointMvcAdapter;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.web.servlet.HandlerExecutionChain;
import org.springframework.web.servlet.HandlerInterceptor;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link CloudFoundryEndpointHandlerMapping}.
*
* @author Madhura Bhave
*/
public class CloudFoundryEndpointHandlerMappingTests {
@Test
public void getHandlerExecutionChainShouldHaveSecurityInterceptor() throws Exception {
TestMvcEndpoint endpoint = new TestMvcEndpoint(new TestEndpoint("a"));
CloudFoundryEndpointHandlerMapping handlerMapping = new CloudFoundryEndpointHandlerMapping(
Arrays.asList(endpoint));
HandlerExecutionChain handlerExecutionChain = handlerMapping
.getHandlerExecutionChain(endpoint, new MockHttpServletRequest());
HandlerInterceptor[] interceptors = handlerExecutionChain.getInterceptors();
assertThat(interceptors).hasAtLeastOneElementOfType(
CloudFoundryEndpointHandlerMapping.SecurityInterceptor.class);
}
@Test
public void getHandlerExecutionChainWhenEndpointHasPathShouldMapAgainstName()
throws Exception {
TestMvcEndpoint testMvcEndpoint = new TestMvcEndpoint(new TestEndpoint("a"));
testMvcEndpoint.setPath("something-else");
CloudFoundryEndpointHandlerMapping handlerMapping = new CloudFoundryEndpointHandlerMapping(
Arrays.asList(testMvcEndpoint));
assertThat(handlerMapping.getPath(testMvcEndpoint)).isEqualTo("/a");
}
private static class TestEndpoint extends AbstractEndpoint<Object> {
TestEndpoint(String id) {
super(id);
}
@Override
public Object invoke() {
return null;
}
}
private static class TestMvcEndpoint extends EndpointMvcAdapter {
TestMvcEndpoint(TestEndpoint delegate) {
super(delegate);
}
}
}

View File

@ -137,15 +137,6 @@ public class EndpointHandlerMappingTests {
assertThat(mapping.getHandler(request("POST", "/a"))).isNull();
}
@Test
public void getEndpointsForSpecifiedType() throws Exception {
TestMvcEndpoint endpoint = new TestMvcEndpoint(new TestEndpoint("a"));
TestActionEndpoint other = new TestActionEndpoint(new TestEndpoint("b"));
EndpointHandlerMapping mapping = new EndpointHandlerMapping(
Arrays.asList(endpoint, other));
assertThat(mapping.getEndpoints(TestMvcEndpoint.class)).containsExactly(endpoint);
}
@Test
public void pathNotMappedWhenGetPathReturnsNull() throws Exception {
TestMvcEndpoint endpoint = new TestMvcEndpoint(new TestEndpoint("a"));

View File

@ -19,6 +19,7 @@ package org.springframework.boot.actuate.endpoint.mvc;
import org.junit.Test;
import org.springframework.boot.actuate.endpoint.AbstractEndpoint;
import org.springframework.boot.actuate.endpoint.Endpoint;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.test.util.EnvironmentTestUtils;
import org.springframework.context.support.StaticApplicationContext;
@ -78,10 +79,21 @@ public class MvcEndpointsTests {
.isEqualTo("/foo/bar");
}
@Test
public void getEndpointsForSpecifiedType() throws Exception {
this.context.getDefaultListableBeanFactory().registerSingleton("endpoint-1",
new TestMvcEndpoint(new TestEndpoint()));
this.context.getDefaultListableBeanFactory().registerSingleton("endpoint-2",
new OtherTestMvcEndpoint(new TestEndpoint()));
this.endpoints.setApplicationContext(this.context);
this.endpoints.afterPropertiesSet();
assertThat(this.endpoints.getEndpoints(TestMvcEndpoint.class)).hasSize(1);
}
@ConfigurationProperties("endpoints.test")
protected static class TestEndpoint extends AbstractEndpoint<String> {
public TestEndpoint() {
TestEndpoint() {
super("test");
}
@ -91,4 +103,18 @@ public class MvcEndpointsTests {
}
}
private static class TestMvcEndpoint extends EndpointMvcAdapter {
TestMvcEndpoint(Endpoint<?> delegate) {
super(delegate);
}
}
private static class OtherTestMvcEndpoint extends EndpointMvcAdapter {
OtherTestMvcEndpoint(Endpoint<?> delegate) {
super(delegate);
}
}
}