Rework Cloud Foundry actuator support behind a pluggable abstraction
Deprecate `EndpointExposure.CLOUD_FOUNDRY` and introduce an alternative implementation based on a pluggable abstraction. The new `EndpointExposureOutcomeContributor` interface may now be used to influence `@OnAvailableEndpointCondition` exposure results. Several infrastructure beans that previously used the condition have been refactored to always be registered, but tolerate missing endpoints. A new smoke test application has been added that demonstrates how the abstraction can be used by a third-party. Closes gh-41135 Co-authored-by: Phillip Webb <phil.webb@broadcom.com>
This commit is contained in:
committed by
Phillip Webb
parent
cbb738338d
commit
73f71d5560
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2022 the original author or authors.
|
||||
* Copyright 2012-2024 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.
|
||||
@@ -52,7 +52,7 @@ public class CachesEndpointAutoConfiguration {
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
@ConditionalOnBean(CachesEndpoint.class)
|
||||
@ConditionalOnAvailableEndpoint(exposure = { EndpointExposure.WEB, EndpointExposure.CLOUD_FOUNDRY })
|
||||
@ConditionalOnAvailableEndpoint(exposure = EndpointExposure.WEB)
|
||||
public CachesEndpointWebExtension cachesEndpointWebExtension(CachesEndpoint cachesEndpoint) {
|
||||
return new CachesEndpointWebExtension(cachesEndpoint);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright 2012-2024 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.cloudfoundry;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.EndpointExposureOutcomeContributor;
|
||||
import org.springframework.boot.actuate.autoconfigure.endpoint.expose.EndpointExposure;
|
||||
import org.springframework.boot.actuate.autoconfigure.endpoint.expose.IncludeExcludeEndpointFilter;
|
||||
import org.springframework.boot.actuate.endpoint.EndpointId;
|
||||
import org.springframework.boot.actuate.endpoint.ExposableEndpoint;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionMessage.Builder;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
|
||||
import org.springframework.boot.cloud.CloudPlatform;
|
||||
import org.springframework.core.env.Environment;
|
||||
|
||||
/**
|
||||
* {@link EndpointExposureOutcomeContributor} to expose {@link EndpointExposure#WEB web}
|
||||
* endpoints for Cloud Foundry.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class CloudFoundryEndpointExposureOutcomeContributor implements EndpointExposureOutcomeContributor {
|
||||
|
||||
private static final String PROPERTY = "management.endpoints.cloud-foundry.exposure";
|
||||
|
||||
private final IncludeExcludeEndpointFilter<?> filter;
|
||||
|
||||
CloudFoundryEndpointExposureOutcomeContributor(Environment environment) {
|
||||
this.filter = (!CloudPlatform.CLOUD_FOUNDRY.isActive(environment)) ? null
|
||||
: new IncludeExcludeEndpointFilter<>(ExposableEndpoint.class, environment, PROPERTY, "*");
|
||||
}
|
||||
|
||||
@Override
|
||||
public ConditionOutcome getExposureOutcome(EndpointId endpointId, Set<EndpointExposure> exposures,
|
||||
Builder message) {
|
||||
if (exposures.contains(EndpointExposure.WEB) && this.filter != null && this.filter.match(endpointId)) {
|
||||
return ConditionOutcome.match(message.because("marked as exposed by a '" + PROPERTY + "' property"));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2022 the original author or authors.
|
||||
* Copyright 2012-2024 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.
|
||||
@@ -55,7 +55,7 @@ public class ConfigurationPropertiesReportEndpointAutoConfiguration {
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
@ConditionalOnBean(ConfigurationPropertiesReportEndpoint.class)
|
||||
@ConditionalOnAvailableEndpoint(exposure = { EndpointExposure.WEB, EndpointExposure.CLOUD_FOUNDRY })
|
||||
@ConditionalOnAvailableEndpoint(exposure = EndpointExposure.WEB)
|
||||
public ConfigurationPropertiesReportEndpointWebExtension configurationPropertiesReportEndpointWebExtension(
|
||||
ConfigurationPropertiesReportEndpoint configurationPropertiesReportEndpoint,
|
||||
ConfigurationPropertiesReportEndpointProperties properties) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2021 the original author or authors.
|
||||
* Copyright 2012-2024 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.
|
||||
@@ -31,15 +31,18 @@ import org.springframework.core.env.Environment;
|
||||
/**
|
||||
* {@link Conditional @Conditional} that checks whether an endpoint is available. An
|
||||
* endpoint is considered available if it is both enabled and exposed on the specified
|
||||
* technologies. Matches enablement according to the endpoints specific
|
||||
* {@link Environment} property, falling back to
|
||||
* {@code management.endpoints.enabled-by-default} or failing that
|
||||
* {@link Endpoint#enableByDefault()}. Matches exposure according to any of the
|
||||
* {@code management.endpoints.web.exposure.<id>} or
|
||||
* {@code management.endpoints.jmx.exposure.<id>} specific properties or failing that to
|
||||
* whether the application runs on
|
||||
* {@link org.springframework.boot.cloud.CloudPlatform#CLOUD_FOUNDRY}. Both those
|
||||
* conditions should match for the endpoint to be considered available.
|
||||
* technologies.
|
||||
* <p>
|
||||
* Matches enablement according to the endpoints specific {@link Environment} property,
|
||||
* falling back to {@code management.endpoints.enabled-by-default} or failing that
|
||||
* {@link Endpoint#enableByDefault()}.
|
||||
* <p>
|
||||
* Matches exposure according to any of the {@code management.endpoints.web.exposure.<id>}
|
||||
* or {@code management.endpoints.jmx.exposure.<id>} specific properties or failing that
|
||||
* to whether any {@link EndpointExposureOutcomeContributor} exposes the endpoint.
|
||||
* <p>
|
||||
* Both enablement and exposure conditions should match for the endpoint to be considered
|
||||
* available.
|
||||
* <p>
|
||||
* When placed on a {@code @Bean} method, the endpoint defaults to the return type of the
|
||||
* factory method:
|
||||
@@ -97,6 +100,8 @@ import org.springframework.core.env.Environment;
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @author Stephane Nicoll
|
||||
* @author Andy Wilkinson
|
||||
* @author Phillip Webb
|
||||
* @since 2.2.0
|
||||
* @see Endpoint
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright 2012-2024 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.endpoint.condition;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.boot.actuate.autoconfigure.endpoint.expose.EndpointExposure;
|
||||
import org.springframework.boot.actuate.endpoint.EndpointId;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionMessage;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
|
||||
import org.springframework.core.env.Environment;
|
||||
|
||||
/**
|
||||
* Contributor loaded from the {@code spring.factories} file and used by
|
||||
* {@link ConditionalOnAvailableEndpoint @ConditionalOnAvailableEndpoint} to determine if
|
||||
* an endpoint is exposed. If any contributor returns a {@link ConditionOutcome#isMatch()
|
||||
* matching} {@link ConditionOutcome} then the endpoint is considered exposed.
|
||||
* <p>
|
||||
* Implementations may declare a constructor that accepts an {@link Environment} argument.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
* @author Phillip Webb
|
||||
* @since 3.4.0
|
||||
*/
|
||||
public interface EndpointExposureOutcomeContributor {
|
||||
|
||||
/**
|
||||
* Return if the given endpoint is exposed for the given set of exposure technologies.
|
||||
* @param endpointId the endpoint ID
|
||||
* @param exposures the exposure technologies to check
|
||||
* @param message the condition message builder
|
||||
* @return a {@link ConditionOutcome#isMatch() matching} {@link ConditionOutcome} if
|
||||
* the endpoint is exposed or {@code null} if the contributor should not apply
|
||||
*/
|
||||
ConditionOutcome getExposureOutcome(EndpointId endpointId, Set<EndpointExposure> exposures,
|
||||
ConditionMessage.Builder message);
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
* Copyright 2012-2024 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.
|
||||
@@ -17,9 +17,10 @@
|
||||
package org.springframework.boot.actuate.autoconfigure.endpoint.condition;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
@@ -31,15 +32,17 @@ import org.springframework.boot.actuate.endpoint.ExposableEndpoint;
|
||||
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
|
||||
import org.springframework.boot.actuate.endpoint.annotation.EndpointExtension;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionMessage;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionMessage.Builder;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
|
||||
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
|
||||
import org.springframework.boot.cloud.CloudPlatform;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.ConditionContext;
|
||||
import org.springframework.core.annotation.MergedAnnotation;
|
||||
import org.springframework.core.annotation.MergedAnnotations;
|
||||
import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.core.io.support.SpringFactoriesLoader;
|
||||
import org.springframework.core.io.support.SpringFactoriesLoader.ArgumentResolver;
|
||||
import org.springframework.core.type.AnnotatedTypeMetadata;
|
||||
import org.springframework.core.type.MethodMetadata;
|
||||
import org.springframework.util.Assert;
|
||||
@@ -52,6 +55,7 @@ import org.springframework.util.ConcurrentReferenceHashMap;
|
||||
* @author Brian Clozel
|
||||
* @author Stephane Nicoll
|
||||
* @author Phillip Webb
|
||||
* @author Andy Wilkinson
|
||||
* @see ConditionalOnAvailableEndpoint
|
||||
*/
|
||||
class OnAvailableEndpointCondition extends SpringBootCondition {
|
||||
@@ -60,7 +64,7 @@ class OnAvailableEndpointCondition extends SpringBootCondition {
|
||||
|
||||
private static final String ENABLED_BY_DEFAULT_KEY = "management.endpoints.enabled-by-default";
|
||||
|
||||
private static final Map<Environment, Set<ExposureFilter>> exposureFiltersCache = new ConcurrentReferenceHashMap<>();
|
||||
private static final Map<Environment, Set<EndpointExposureOutcomeContributor>> exposureOutcomeContributorsCache = new ConcurrentReferenceHashMap<>();
|
||||
|
||||
private static final ConcurrentReferenceHashMap<Environment, Optional<Boolean>> enabledByDefaultCache = new ConcurrentReferenceHashMap<>();
|
||||
|
||||
@@ -110,18 +114,10 @@ class OnAvailableEndpointCondition extends SpringBootCondition {
|
||||
ConditionMessage.Builder message = ConditionMessage.forCondition(ConditionalOnAvailableEndpoint.class);
|
||||
EndpointId endpointId = EndpointId.of(environment, endpointAnnotation.getString("id"));
|
||||
ConditionOutcome enablementOutcome = getEnablementOutcome(environment, endpointAnnotation, endpointId, message);
|
||||
if (!enablementOutcome.isMatch()) {
|
||||
return enablementOutcome;
|
||||
}
|
||||
Set<EndpointExposure> exposuresToCheck = getExposuresToCheck(conditionAnnotation);
|
||||
Set<ExposureFilter> exposureFilters = getExposureFilters(environment);
|
||||
for (ExposureFilter exposureFilter : exposureFilters) {
|
||||
if (exposuresToCheck.contains(exposureFilter.getExposure()) && exposureFilter.isExposed(endpointId)) {
|
||||
return ConditionOutcome.match(message.because("marked as exposed by a 'management.endpoints."
|
||||
+ exposureFilter.getExposure().name().toLowerCase() + ".exposure' property"));
|
||||
}
|
||||
}
|
||||
return ConditionOutcome.noMatch(message.because("no 'management.endpoints' property marked it as exposed"));
|
||||
ConditionOutcome exposureOutcome = (!enablementOutcome.isMatch()) ? null
|
||||
: getExposureOutcome(environment, conditionAnnotation, endpointAnnotation, endpointId, message);
|
||||
return (exposureOutcome != null) ? exposureOutcome
|
||||
: ConditionOutcome.noMatch(message.because("not enabled or exposed"));
|
||||
}
|
||||
|
||||
private ConditionOutcome getEnablementOutcome(Environment environment,
|
||||
@@ -148,54 +144,83 @@ class OnAvailableEndpointCondition extends SpringBootCondition {
|
||||
return enabledByDefault.orElse(null);
|
||||
}
|
||||
|
||||
private Set<EndpointExposure> getExposuresToCheck(
|
||||
MergedAnnotation<ConditionalOnAvailableEndpoint> conditionAnnotation) {
|
||||
EndpointExposure[] exposure = conditionAnnotation.getEnumArray("exposure", EndpointExposure.class);
|
||||
return (exposure.length == 0) ? EnumSet.allOf(EndpointExposure.class)
|
||||
: new LinkedHashSet<>(Arrays.asList(exposure));
|
||||
}
|
||||
|
||||
private Set<ExposureFilter> getExposureFilters(Environment environment) {
|
||||
Set<ExposureFilter> exposureFilters = exposureFiltersCache.get(environment);
|
||||
if (exposureFilters == null) {
|
||||
exposureFilters = new HashSet<>(2);
|
||||
if (environment.getProperty(JMX_ENABLED_KEY, Boolean.class, false)) {
|
||||
exposureFilters.add(new ExposureFilter(environment, EndpointExposure.JMX));
|
||||
private ConditionOutcome getExposureOutcome(Environment environment,
|
||||
MergedAnnotation<ConditionalOnAvailableEndpoint> conditionAnnotation,
|
||||
MergedAnnotation<Endpoint> endpointAnnotation, EndpointId endpointId, Builder message) {
|
||||
Set<EndpointExposure> exposures = getExposures(conditionAnnotation);
|
||||
Set<EndpointExposureOutcomeContributor> outcomeContributors = getExposureOutcomeContributors(environment);
|
||||
for (EndpointExposureOutcomeContributor outcomeContributor : outcomeContributors) {
|
||||
ConditionOutcome outcome = outcomeContributor.getExposureOutcome(endpointId, exposures, message);
|
||||
if (outcome != null && outcome.isMatch()) {
|
||||
return outcome;
|
||||
}
|
||||
if (CloudPlatform.CLOUD_FOUNDRY.isActive(environment)) {
|
||||
exposureFilters.add(new ExposureFilter(environment, EndpointExposure.CLOUD_FOUNDRY));
|
||||
}
|
||||
exposureFilters.add(new ExposureFilter(environment, EndpointExposure.WEB));
|
||||
exposureFiltersCache.put(environment, exposureFilters);
|
||||
}
|
||||
return exposureFilters;
|
||||
return null;
|
||||
}
|
||||
|
||||
static final class ExposureFilter extends IncludeExcludeEndpointFilter<ExposableEndpoint<?>> {
|
||||
private Set<EndpointExposure> getExposures(MergedAnnotation<ConditionalOnAvailableEndpoint> conditionAnnotation) {
|
||||
EndpointExposure[] exposures = conditionAnnotation.getEnumArray("exposure", EndpointExposure.class);
|
||||
return replaceCloudFoundryExposure(
|
||||
(exposures.length == 0) ? EnumSet.allOf(EndpointExposure.class) : Arrays.asList(exposures));
|
||||
}
|
||||
|
||||
@SuppressWarnings("removal")
|
||||
private Set<EndpointExposure> replaceCloudFoundryExposure(Collection<EndpointExposure> exposures) {
|
||||
Set<EndpointExposure> result = EnumSet.copyOf(exposures);
|
||||
if (result.remove(EndpointExposure.CLOUD_FOUNDRY)) {
|
||||
result.add(EndpointExposure.WEB);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private Set<EndpointExposureOutcomeContributor> getExposureOutcomeContributors(Environment environment) {
|
||||
Set<EndpointExposureOutcomeContributor> contributors = exposureOutcomeContributorsCache.get(environment);
|
||||
if (contributors == null) {
|
||||
contributors = new LinkedHashSet<>();
|
||||
contributors.add(new StandardExposureOutcomeContributor(environment, EndpointExposure.WEB));
|
||||
if (environment.getProperty(JMX_ENABLED_KEY, Boolean.class, false)) {
|
||||
contributors.add(new StandardExposureOutcomeContributor(environment, EndpointExposure.JMX));
|
||||
}
|
||||
contributors.addAll(loadExposureOutcomeContributors(environment));
|
||||
exposureOutcomeContributorsCache.put(environment, contributors);
|
||||
}
|
||||
return contributors;
|
||||
}
|
||||
|
||||
private List<EndpointExposureOutcomeContributor> loadExposureOutcomeContributors(Environment environment) {
|
||||
ArgumentResolver argumentResolver = ArgumentResolver.of(Environment.class, environment);
|
||||
return SpringFactoriesLoader.forDefaultResourceLocation()
|
||||
.load(EndpointExposureOutcomeContributor.class, argumentResolver);
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard {@link EndpointExposureOutcomeContributor}.
|
||||
*/
|
||||
private static class StandardExposureOutcomeContributor implements EndpointExposureOutcomeContributor {
|
||||
|
||||
private final EndpointExposure exposure;
|
||||
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
private ExposureFilter(Environment environment, EndpointExposure exposure) {
|
||||
super((Class) ExposableEndpoint.class, environment,
|
||||
"management.endpoints." + getCanonicalName(exposure) + ".exposure", exposure.getDefaultIncludes());
|
||||
private final String property;
|
||||
|
||||
private final IncludeExcludeEndpointFilter<?> filter;
|
||||
|
||||
StandardExposureOutcomeContributor(Environment environment, EndpointExposure exposure) {
|
||||
this.exposure = exposure;
|
||||
String name = exposure.name().toLowerCase().replace('_', '-');
|
||||
this.property = "management.endpoints." + name + ".exposure";
|
||||
this.filter = new IncludeExcludeEndpointFilter<>(ExposableEndpoint.class, environment, this.property,
|
||||
exposure.getDefaultIncludes());
|
||||
|
||||
}
|
||||
|
||||
private static String getCanonicalName(EndpointExposure exposure) {
|
||||
if (EndpointExposure.CLOUD_FOUNDRY.equals(exposure)) {
|
||||
return "cloud-foundry";
|
||||
@Override
|
||||
public ConditionOutcome getExposureOutcome(EndpointId endpointId, Set<EndpointExposure> exposures,
|
||||
ConditionMessage.Builder message) {
|
||||
if (exposures.contains(this.exposure) && this.filter.match(endpointId)) {
|
||||
return ConditionOutcome
|
||||
.match(message.because("marked as exposed by a '" + this.property + "' property"));
|
||||
}
|
||||
return exposure.name().toLowerCase();
|
||||
}
|
||||
|
||||
EndpointExposure getExposure() {
|
||||
return this.exposure;
|
||||
}
|
||||
|
||||
boolean isExposed(EndpointId id) {
|
||||
return super.match(id);
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
* Copyright 2012-2024 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.
|
||||
@@ -37,7 +37,10 @@ public enum EndpointExposure {
|
||||
/**
|
||||
* Exposed on Cloud Foundry over `/cloudfoundryapplication`.
|
||||
* @since 2.6.4
|
||||
* @deprecated since 3.4.0 for removal in 3.6.0 in favor of using
|
||||
* {@link EndpointExposure#WEB}
|
||||
*/
|
||||
@Deprecated(since = "3.4.0", forRemoval = true)
|
||||
CLOUD_FOUNDRY("*");
|
||||
|
||||
private final String[] defaultIncludes;
|
||||
|
||||
@@ -100,12 +100,12 @@ class JerseyWebEndpointManagementContextConfiguration {
|
||||
JerseyAdditionalHealthEndpointPathsManagementResourcesRegistrar jerseyDifferentPortAdditionalHealthEndpointPathsResourcesRegistrar(
|
||||
WebEndpointsSupplier webEndpointsSupplier, HealthEndpointGroups healthEndpointGroups) {
|
||||
Collection<ExposableWebEndpoint> webEndpoints = webEndpointsSupplier.getEndpoints();
|
||||
ExposableWebEndpoint health = webEndpoints.stream()
|
||||
ExposableWebEndpoint healthEndpoint = webEndpoints.stream()
|
||||
.filter((endpoint) -> endpoint.getEndpointId().equals(HEALTH_ENDPOINT_ID))
|
||||
.findFirst()
|
||||
.orElseThrow(
|
||||
() -> new IllegalStateException("No endpoint with id '%s' found".formatted(HEALTH_ENDPOINT_ID)));
|
||||
return new JerseyAdditionalHealthEndpointPathsManagementResourcesRegistrar(health, healthEndpointGroups);
|
||||
.orElse(null);
|
||||
return new JerseyAdditionalHealthEndpointPathsManagementResourcesRegistrar(healthEndpoint,
|
||||
healthEndpointGroups);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@@ -180,19 +180,21 @@ class JerseyWebEndpointManagementContextConfiguration {
|
||||
class JerseyAdditionalHealthEndpointPathsManagementResourcesRegistrar
|
||||
implements ManagementContextResourceConfigCustomizer {
|
||||
|
||||
private final ExposableWebEndpoint endpoint;
|
||||
private final ExposableWebEndpoint healthEndpoint;
|
||||
|
||||
private final HealthEndpointGroups groups;
|
||||
|
||||
JerseyAdditionalHealthEndpointPathsManagementResourcesRegistrar(ExposableWebEndpoint endpoint,
|
||||
JerseyAdditionalHealthEndpointPathsManagementResourcesRegistrar(ExposableWebEndpoint healthEndpoint,
|
||||
HealthEndpointGroups groups) {
|
||||
this.endpoint = endpoint;
|
||||
this.healthEndpoint = healthEndpoint;
|
||||
this.groups = groups;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void customize(ResourceConfig config) {
|
||||
register(config);
|
||||
if (this.healthEndpoint != null) {
|
||||
register(config);
|
||||
}
|
||||
}
|
||||
|
||||
private void register(ResourceConfig config) {
|
||||
@@ -200,7 +202,7 @@ class JerseyWebEndpointManagementContextConfiguration {
|
||||
JerseyHealthEndpointAdditionalPathResourceFactory resourceFactory = new JerseyHealthEndpointAdditionalPathResourceFactory(
|
||||
WebServerNamespace.MANAGEMENT, this.groups);
|
||||
Collection<Resource> endpointResources = resourceFactory
|
||||
.createEndpointResources(mapping, Collections.singletonList(this.endpoint))
|
||||
.createEndpointResources(mapping, Collections.singletonList(this.healthEndpoint))
|
||||
.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.toList();
|
||||
|
||||
@@ -117,12 +117,11 @@ public class WebFluxEndpointManagementContextConfiguration {
|
||||
public AdditionalHealthEndpointPathsWebFluxHandlerMapping managementHealthEndpointWebFluxHandlerMapping(
|
||||
WebEndpointsSupplier webEndpointsSupplier, HealthEndpointGroups groups) {
|
||||
Collection<ExposableWebEndpoint> webEndpoints = webEndpointsSupplier.getEndpoints();
|
||||
ExposableWebEndpoint health = webEndpoints.stream()
|
||||
ExposableWebEndpoint healthEndpoint = webEndpoints.stream()
|
||||
.filter((endpoint) -> endpoint.getEndpointId().equals(HealthEndpoint.ID))
|
||||
.findFirst()
|
||||
.orElseThrow(
|
||||
() -> new IllegalStateException("No endpoint with id '%s' found".formatted(HealthEndpoint.ID)));
|
||||
return new AdditionalHealthEndpointPathsWebFluxHandlerMapping(new EndpointMapping(""), health,
|
||||
.orElse(null);
|
||||
return new AdditionalHealthEndpointPathsWebFluxHandlerMapping(new EndpointMapping(""), healthEndpoint,
|
||||
groups.getAllWithAdditionalPath(WebServerNamespace.MANAGEMENT));
|
||||
}
|
||||
|
||||
|
||||
@@ -112,15 +112,18 @@ public class WebMvcEndpointManagementContextConfiguration {
|
||||
public AdditionalHealthEndpointPathsWebMvcHandlerMapping managementHealthEndpointWebMvcHandlerMapping(
|
||||
WebEndpointsSupplier webEndpointsSupplier, HealthEndpointGroups groups) {
|
||||
Collection<ExposableWebEndpoint> webEndpoints = webEndpointsSupplier.getEndpoints();
|
||||
ExposableWebEndpoint health = webEndpoints.stream()
|
||||
.filter((endpoint) -> endpoint.getEndpointId().equals(HealthEndpoint.ID))
|
||||
ExposableWebEndpoint healthEndpoint = webEndpoints.stream()
|
||||
.filter(this::isHealthEndpoint)
|
||||
.findFirst()
|
||||
.orElseThrow(
|
||||
() -> new IllegalStateException("No endpoint with id '%s' found".formatted(HealthEndpoint.ID)));
|
||||
return new AdditionalHealthEndpointPathsWebMvcHandlerMapping(health,
|
||||
.orElse(null);
|
||||
return new AdditionalHealthEndpointPathsWebMvcHandlerMapping(healthEndpoint,
|
||||
groups.getAllWithAdditionalPath(WebServerNamespace.MANAGEMENT));
|
||||
}
|
||||
|
||||
private boolean isHealthEndpoint(ExposableWebEndpoint endpoint) {
|
||||
return endpoint.getEndpointId().equals(HealthEndpoint.ID);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
@SuppressWarnings("removal")
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2022 the original author or authors.
|
||||
* Copyright 2012-2024 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.
|
||||
@@ -53,7 +53,7 @@ public class EnvironmentEndpointAutoConfiguration {
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
@ConditionalOnBean(EnvironmentEndpoint.class)
|
||||
@ConditionalOnAvailableEndpoint(exposure = { EndpointExposure.WEB, EndpointExposure.CLOUD_FOUNDRY })
|
||||
@ConditionalOnAvailableEndpoint(exposure = EndpointExposure.WEB)
|
||||
public EnvironmentEndpointWebExtension environmentEndpointWebExtension(EnvironmentEndpoint environmentEndpoint,
|
||||
EnvironmentEndpointProperties properties) {
|
||||
return new EnvironmentEndpointWebExtension(environmentEndpoint, properties.getShowValues(),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
* Copyright 2012-2024 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.
|
||||
@@ -45,8 +45,7 @@ import org.springframework.context.annotation.Configuration;
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnWebApplication(type = Type.REACTIVE)
|
||||
@ConditionalOnAvailableEndpoint(endpoint = HealthEndpoint.class,
|
||||
exposure = { EndpointExposure.WEB, EndpointExposure.CLOUD_FOUNDRY })
|
||||
@ConditionalOnAvailableEndpoint(endpoint = HealthEndpoint.class, exposure = EndpointExposure.WEB)
|
||||
class HealthEndpointReactiveWebExtensionConfiguration {
|
||||
|
||||
@Bean
|
||||
@@ -60,7 +59,6 @@ class HealthEndpointReactiveWebExtensionConfiguration {
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnAvailableEndpoint(endpoint = HealthEndpoint.class, exposure = EndpointExposure.WEB)
|
||||
static class WebFluxAdditionalHealthEndpointPathsConfiguration {
|
||||
|
||||
@Bean
|
||||
@@ -70,8 +68,7 @@ class HealthEndpointReactiveWebExtensionConfiguration {
|
||||
ExposableWebEndpoint health = webEndpoints.stream()
|
||||
.filter((endpoint) -> endpoint.getEndpointId().equals(HealthEndpoint.ID))
|
||||
.findFirst()
|
||||
.orElseThrow(
|
||||
() -> new IllegalStateException("No endpoint with id '%s' found".formatted(HealthEndpoint.ID)));
|
||||
.orElse(null);
|
||||
return new AdditionalHealthEndpointPathsWebFluxHandlerMapping(new EndpointMapping(""), health,
|
||||
groups.getAllWithAdditionalPath(WebServerNamespace.SERVER));
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
* Copyright 2012-2024 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.
|
||||
@@ -64,8 +64,7 @@ import org.springframework.web.servlet.DispatcherServlet;
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnWebApplication(type = Type.SERVLET)
|
||||
@ConditionalOnBean(HealthEndpoint.class)
|
||||
@ConditionalOnAvailableEndpoint(endpoint = HealthEndpoint.class,
|
||||
exposure = { EndpointExposure.WEB, EndpointExposure.CLOUD_FOUNDRY })
|
||||
@ConditionalOnAvailableEndpoint(endpoint = HealthEndpoint.class, exposure = EndpointExposure.WEB)
|
||||
class HealthEndpointWebExtensionConfiguration {
|
||||
|
||||
@Bean
|
||||
@@ -81,12 +80,10 @@ class HealthEndpointWebExtensionConfiguration {
|
||||
return webEndpoints.stream()
|
||||
.filter((endpoint) -> endpoint.getEndpointId().equals(HealthEndpoint.ID))
|
||||
.findFirst()
|
||||
.orElseThrow(
|
||||
() -> new IllegalStateException("No endpoint with id '%s' found".formatted(HealthEndpoint.ID)));
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
@ConditionalOnBean(DispatcherServlet.class)
|
||||
@ConditionalOnAvailableEndpoint(endpoint = HealthEndpoint.class, exposure = EndpointExposure.WEB)
|
||||
static class MvcAdditionalHealthEndpointPathsConfiguration {
|
||||
|
||||
@Bean
|
||||
@@ -102,7 +99,6 @@ class HealthEndpointWebExtensionConfiguration {
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnClass(ResourceConfig.class)
|
||||
@ConditionalOnMissingClass("org.springframework.web.servlet.DispatcherServlet")
|
||||
@ConditionalOnAvailableEndpoint(endpoint = HealthEndpoint.class, exposure = EndpointExposure.WEB)
|
||||
static class JerseyAdditionalHealthEndpointPathsConfiguration {
|
||||
|
||||
@Bean
|
||||
@@ -163,7 +159,8 @@ class HealthEndpointWebExtensionConfiguration {
|
||||
JerseyHealthEndpointAdditionalPathResourceFactory resourceFactory = new JerseyHealthEndpointAdditionalPathResourceFactory(
|
||||
WebServerNamespace.SERVER, this.groups);
|
||||
Collection<Resource> endpointResources = resourceFactory
|
||||
.createEndpointResources(mapping, Collections.singletonList(this.endpoint))
|
||||
.createEndpointResources(mapping,
|
||||
(this.endpoint != null) ? Collections.singletonList(this.endpoint) : Collections.emptyList())
|
||||
.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.toList();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2022 the original author or authors.
|
||||
* Copyright 2012-2024 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.
|
||||
@@ -56,7 +56,7 @@ public class QuartzEndpointAutoConfiguration {
|
||||
@Bean
|
||||
@ConditionalOnBean(QuartzEndpoint.class)
|
||||
@ConditionalOnMissingBean
|
||||
@ConditionalOnAvailableEndpoint(exposure = { EndpointExposure.WEB, EndpointExposure.CLOUD_FOUNDRY })
|
||||
@ConditionalOnAvailableEndpoint(exposure = EndpointExposure.WEB)
|
||||
public QuartzEndpointWebExtension quartzEndpointWebExtension(QuartzEndpoint endpoint,
|
||||
QuartzEndpointProperties properties) {
|
||||
return new QuartzEndpointWebExtension(endpoint, properties.getShowValues(), properties.getRoles());
|
||||
|
||||
@@ -55,7 +55,7 @@ public class SbomEndpointAutoConfiguration {
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
@ConditionalOnBean(SbomEndpoint.class)
|
||||
@ConditionalOnAvailableEndpoint(exposure = { EndpointExposure.WEB, EndpointExposure.CLOUD_FOUNDRY })
|
||||
@ConditionalOnAvailableEndpoint(exposure = EndpointExposure.WEB)
|
||||
SbomEndpointWebExtension sbomEndpointWebExtension(SbomEndpoint sbomEndpoint) {
|
||||
return new SbomEndpointWebExtension(sbomEndpoint, this.properties);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
# Endpoint Exposure Outcome Contributors
|
||||
org.springframework.boot.actuate.autoconfigure.endpoint.condition.EndpointExposureOutcomeContributor=\
|
||||
org.springframework.boot.actuate.autoconfigure.cloudfoundry.CloudFoundryEndpointExposureOutcomeContributor
|
||||
|
||||
# Failure Analyzers
|
||||
org.springframework.boot.diagnostics.FailureAnalyzer=\
|
||||
org.springframework.boot.actuate.autoconfigure.health.NoSuchHealthContributorFailureAnalyzer,\
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
* Copyright 2012-2024 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.
|
||||
@@ -374,8 +374,7 @@ class ConditionalOnAvailableEndpointTests {
|
||||
static class ExposureEndpointConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnAvailableEndpoint(endpoint = TestEndpoint.class,
|
||||
exposure = { EndpointExposure.WEB, EndpointExposure.CLOUD_FOUNDRY })
|
||||
@ConditionalOnAvailableEndpoint(endpoint = TestEndpoint.class, exposure = EndpointExposure.WEB)
|
||||
String unexposed() {
|
||||
return "unexposed";
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2023 the original author or authors.
|
||||
* Copyright 2012-2024 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.
|
||||
@@ -26,6 +26,9 @@ import reactor.core.publisher.Mono;
|
||||
import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration;
|
||||
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration;
|
||||
import org.springframework.boot.actuate.autoconfigure.health.HealthEndpointConfiguration.HealthEndpointGroupMembershipValidator.NoSuchHealthContributorException;
|
||||
import org.springframework.boot.actuate.autoconfigure.health.HealthEndpointReactiveWebExtensionConfiguration.WebFluxAdditionalHealthEndpointPathsConfiguration;
|
||||
import org.springframework.boot.actuate.autoconfigure.health.HealthEndpointWebExtensionConfiguration.JerseyAdditionalHealthEndpointPathsConfiguration;
|
||||
import org.springframework.boot.actuate.autoconfigure.health.HealthEndpointWebExtensionConfiguration.MvcAdditionalHealthEndpointPathsConfiguration;
|
||||
import org.springframework.boot.actuate.endpoint.ApiVersion;
|
||||
import org.springframework.boot.actuate.endpoint.SecurityContext;
|
||||
import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse;
|
||||
@@ -50,12 +53,16 @@ import org.springframework.boot.actuate.health.Status;
|
||||
import org.springframework.boot.actuate.health.StatusAggregator;
|
||||
import org.springframework.boot.actuate.health.SystemHealth;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener;
|
||||
import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration;
|
||||
import org.springframework.boot.logging.LogLevel;
|
||||
import org.springframework.boot.test.context.FilteredClassLoader;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
|
||||
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.DispatcherServlet;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
@@ -337,6 +344,47 @@ class HealthEndpointAutoConfigurationTests {
|
||||
}));
|
||||
}
|
||||
|
||||
@Test
|
||||
void additionalHealthEndpointsPathsTolerateHealthEndpointThatIsNotWebExposed() {
|
||||
this.contextRunner
|
||||
.withConfiguration(AutoConfigurations.of(DispatcherServletAutoConfiguration.class,
|
||||
EndpointAutoConfiguration.class, WebEndpointAutoConfiguration.class))
|
||||
.withPropertyValues("management.endpoints.web.exposure.exclude=*",
|
||||
"management.endpoints.cloudfoundry.exposure.include=*", "spring.main.cloud-platform=cloud_foundry")
|
||||
.run((context) -> {
|
||||
assertThat(context).hasSingleBean(MvcAdditionalHealthEndpointPathsConfiguration.class);
|
||||
assertThat(context).hasNotFailed();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void additionalJerseyHealthEndpointsPathsTolerateHealthEndpointThatIsNotWebExposed() {
|
||||
this.contextRunner
|
||||
.withConfiguration(
|
||||
AutoConfigurations.of(EndpointAutoConfiguration.class, WebEndpointAutoConfiguration.class))
|
||||
.withClassLoader(new FilteredClassLoader(DispatcherServlet.class))
|
||||
.withPropertyValues("management.endpoints.web.exposure.exclude=*",
|
||||
"management.endpoints.cloudfoundry.exposure.include=*", "spring.main.cloud-platform=cloud_foundry")
|
||||
.withInitializer(ConditionEvaluationReportLoggingListener.forLogLevel(LogLevel.INFO))
|
||||
.run((context) -> {
|
||||
assertThat(context).hasSingleBean(JerseyAdditionalHealthEndpointPathsConfiguration.class);
|
||||
assertThat(context).hasNotFailed();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void additionalReactiveHealthEndpointsPathsTolerateHealthEndpointThatIsNotWebExposed() {
|
||||
this.reactiveContextRunner
|
||||
.withConfiguration(
|
||||
AutoConfigurations.of(EndpointAutoConfiguration.class, WebEndpointAutoConfiguration.class))
|
||||
.withPropertyValues("management.endpoints.web.exposure.exclude=*",
|
||||
"management.endpoints.cloudfoundry.exposure.include=*", "spring.main.cloud-platform=cloud_foundry")
|
||||
.run((context) -> {
|
||||
assertThat(context).hasSingleBean(WebFluxAdditionalHealthEndpointPathsConfiguration.class);
|
||||
assertThat(context).hasNotFailed();
|
||||
});
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class HealthIndicatorsConfiguration {
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2021 the original author or authors.
|
||||
* Copyright 2012-2024 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.
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.boot.actuate.endpoint.web.reactive;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
|
||||
@@ -41,21 +42,28 @@ public class AdditionalHealthEndpointPathsWebFluxHandlerMapping extends Abstract
|
||||
|
||||
private final EndpointMapping endpointMapping;
|
||||
|
||||
private final ExposableWebEndpoint endpoint;
|
||||
private final ExposableWebEndpoint healthEndpoint;
|
||||
|
||||
private final Set<HealthEndpointGroup> groups;
|
||||
|
||||
public AdditionalHealthEndpointPathsWebFluxHandlerMapping(EndpointMapping endpointMapping,
|
||||
ExposableWebEndpoint endpoint, Set<HealthEndpointGroup> groups) {
|
||||
super(endpointMapping, Collections.singletonList(endpoint), null, null, false);
|
||||
ExposableWebEndpoint healthEndpoint, Set<HealthEndpointGroup> groups) {
|
||||
super(endpointMapping, asList(healthEndpoint), null, null, false);
|
||||
this.endpointMapping = endpointMapping;
|
||||
this.groups = groups;
|
||||
this.endpoint = endpoint;
|
||||
this.healthEndpoint = healthEndpoint;
|
||||
}
|
||||
|
||||
private static Collection<ExposableWebEndpoint> asList(ExposableWebEndpoint healthEndpoint) {
|
||||
return (healthEndpoint != null) ? Collections.singletonList(healthEndpoint) : Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initHandlerMethods() {
|
||||
for (WebOperation operation : this.endpoint.getOperations()) {
|
||||
if (this.healthEndpoint == null) {
|
||||
return;
|
||||
}
|
||||
for (WebOperation operation : this.healthEndpoint.getOperations()) {
|
||||
WebOperationRequestPredicate predicate = operation.getRequestPredicate();
|
||||
String matchAllRemainingPathSegmentsVariable = predicate.getMatchAllRemainingPathSegmentsVariable();
|
||||
if (matchAllRemainingPathSegmentsVariable != null) {
|
||||
@@ -64,7 +72,7 @@ public class AdditionalHealthEndpointPathsWebFluxHandlerMapping extends Abstract
|
||||
if (additionalPath != null) {
|
||||
RequestMappingInfo requestMappingInfo = getRequestMappingInfo(operation,
|
||||
additionalPath.getValue());
|
||||
registerReadMapping(requestMappingInfo, this.endpoint, operation);
|
||||
registerReadMapping(requestMappingInfo, this.healthEndpoint, operation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2021 the original author or authors.
|
||||
* Copyright 2012-2024 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.
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.boot.actuate.endpoint.web.servlet;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
|
||||
@@ -36,27 +37,34 @@ import org.springframework.web.servlet.HandlerMapping;
|
||||
*/
|
||||
public class AdditionalHealthEndpointPathsWebMvcHandlerMapping extends AbstractWebMvcEndpointHandlerMapping {
|
||||
|
||||
private final ExposableWebEndpoint endpoint;
|
||||
private final ExposableWebEndpoint healthEndpoint;
|
||||
|
||||
private final Set<HealthEndpointGroup> groups;
|
||||
|
||||
public AdditionalHealthEndpointPathsWebMvcHandlerMapping(ExposableWebEndpoint endpoint,
|
||||
public AdditionalHealthEndpointPathsWebMvcHandlerMapping(ExposableWebEndpoint healthEndpoint,
|
||||
Set<HealthEndpointGroup> groups) {
|
||||
super(new EndpointMapping(""), Collections.singletonList(endpoint), null, false);
|
||||
this.endpoint = endpoint;
|
||||
super(new EndpointMapping(""), asList(healthEndpoint), null, false);
|
||||
this.healthEndpoint = healthEndpoint;
|
||||
this.groups = groups;
|
||||
}
|
||||
|
||||
private static Collection<ExposableWebEndpoint> asList(ExposableWebEndpoint healthEndpoint) {
|
||||
return (healthEndpoint != null) ? Collections.singletonList(healthEndpoint) : Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initHandlerMethods() {
|
||||
for (WebOperation operation : this.endpoint.getOperations()) {
|
||||
if (this.healthEndpoint == null) {
|
||||
return;
|
||||
}
|
||||
for (WebOperation operation : this.healthEndpoint.getOperations()) {
|
||||
WebOperationRequestPredicate predicate = operation.getRequestPredicate();
|
||||
String matchAllRemainingPathSegmentsVariable = predicate.getMatchAllRemainingPathSegmentsVariable();
|
||||
if (matchAllRemainingPathSegmentsVariable != null) {
|
||||
for (HealthEndpointGroup group : this.groups) {
|
||||
AdditionalHealthEndpointPath additionalPath = group.getAdditionalPath();
|
||||
if (additionalPath != null) {
|
||||
registerMapping(this.endpoint, predicate, operation, additionalPath.getValue());
|
||||
registerMapping(this.healthEndpoint, predicate, operation, additionalPath.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
plugins {
|
||||
id "java"
|
||||
id "org.springframework.boot.conventions"
|
||||
}
|
||||
|
||||
description = "Spring Boot Actuator Extension smoke test"
|
||||
|
||||
dependencies {
|
||||
implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-actuator"))
|
||||
implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-web"))
|
||||
|
||||
testImplementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-test"))
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright 2012-2024 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 smoketest.actuator.extension;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.actuate.autoconfigure.endpoint.web.CorsEndpointProperties;
|
||||
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties;
|
||||
import org.springframework.boot.actuate.endpoint.EndpointFilter;
|
||||
import org.springframework.boot.actuate.endpoint.invoke.OperationInvokerAdvisor;
|
||||
import org.springframework.boot.actuate.endpoint.invoke.ParameterValueMapper;
|
||||
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
|
||||
import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint;
|
||||
import org.springframework.boot.actuate.endpoint.web.WebEndpointsSupplier;
|
||||
import org.springframework.boot.actuate.endpoint.web.annotation.WebEndpointDiscoverer;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
public class MyExtensionConfiguration {
|
||||
|
||||
@Bean
|
||||
public MyExtensionWebMvcEndpointHandlerMapping myWebMvcEndpointHandlerMapping(
|
||||
WebEndpointsSupplier webEndpointsSupplier, EndpointMediaTypes endpointMediaTypes,
|
||||
ObjectProvider<CorsEndpointProperties> corsPropertiesProvider, WebEndpointProperties webEndpointProperties,
|
||||
Environment environment, ApplicationContext applicationContext, ParameterValueMapper parameterMapper) {
|
||||
CorsEndpointProperties corsProperties = corsPropertiesProvider.getIfAvailable();
|
||||
CorsConfiguration corsConfiguration = (corsProperties != null) ? corsProperties.toCorsConfiguration() : null;
|
||||
List<OperationInvokerAdvisor> invokerAdvisors = Collections.emptyList();
|
||||
List<EndpointFilter<ExposableWebEndpoint>> filters = Collections
|
||||
.singletonList(new MyExtensionEndpointFilter(environment));
|
||||
WebEndpointDiscoverer discoverer = new WebEndpointDiscoverer(applicationContext, parameterMapper,
|
||||
endpointMediaTypes, null, invokerAdvisors, filters);
|
||||
Collection<ExposableWebEndpoint> endpoints = discoverer.getEndpoints();
|
||||
return new MyExtensionWebMvcEndpointHandlerMapping(endpoints, endpointMediaTypes, corsConfiguration);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2012-2024 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 smoketest.actuator.extension;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.EndpointExposureOutcomeContributor;
|
||||
import org.springframework.boot.actuate.autoconfigure.endpoint.expose.EndpointExposure;
|
||||
import org.springframework.boot.actuate.endpoint.EndpointId;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionMessage.Builder;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
|
||||
import org.springframework.core.env.Environment;
|
||||
|
||||
class MyExtensionEndpointExposureOutcomeContributor implements EndpointExposureOutcomeContributor {
|
||||
|
||||
private final MyExtensionEndpointFilter filter;
|
||||
|
||||
MyExtensionEndpointExposureOutcomeContributor(Environment environment) {
|
||||
this.filter = new MyExtensionEndpointFilter(environment);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ConditionOutcome getExposureOutcome(EndpointId endpointId, Set<EndpointExposure> exposures,
|
||||
Builder message) {
|
||||
if (exposures.contains(EndpointExposure.WEB) && this.filter.match(endpointId)) {
|
||||
return ConditionOutcome.match(message.because("marked as exposed by a my extension '"
|
||||
+ MyExtensionEndpointFilter.PROPERTY_PREFIX + "' property"));
|
||||
}
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2012-2024 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 smoketest.actuator.extension;
|
||||
|
||||
import org.springframework.boot.actuate.autoconfigure.endpoint.expose.IncludeExcludeEndpointFilter;
|
||||
import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint;
|
||||
import org.springframework.core.env.Environment;
|
||||
|
||||
class MyExtensionEndpointFilter extends IncludeExcludeEndpointFilter<ExposableWebEndpoint> {
|
||||
|
||||
static final String PROPERTY_PREFIX = "management.endpoints.myextension.exposure";
|
||||
|
||||
MyExtensionEndpointFilter(Environment environment) {
|
||||
super(ExposableWebEndpoint.class, environment, PROPERTY_PREFIX, "*");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 2012-2024 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 smoketest.actuator.extension;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
|
||||
class MyExtensionSecurityInterceptor implements HandlerInterceptor {
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
|
||||
throws Exception {
|
||||
String auth = request.getHeader("Authorization");
|
||||
if (!"Bearer secret".equals(auth)) {
|
||||
response.sendError(HttpStatus.UNAUTHORIZED.value());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright 2012-2024 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 smoketest.actuator.extension;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver;
|
||||
import org.springframework.boot.actuate.endpoint.web.EndpointMapping;
|
||||
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
|
||||
import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint;
|
||||
import org.springframework.boot.actuate.endpoint.web.Link;
|
||||
import org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
|
||||
class MyExtensionWebMvcEndpointHandlerMapping extends AbstractWebMvcEndpointHandlerMapping {
|
||||
|
||||
private static final String PATH = "/myextension";
|
||||
|
||||
private final EndpointLinksResolver linksResolver;
|
||||
|
||||
MyExtensionWebMvcEndpointHandlerMapping(Collection<ExposableWebEndpoint> endpoints,
|
||||
EndpointMediaTypes endpointMediaTypes, CorsConfiguration corsConfiguration) {
|
||||
super(new EndpointMapping(PATH), endpoints, endpointMediaTypes, corsConfiguration, true);
|
||||
this.linksResolver = new EndpointLinksResolver(endpoints, PATH);
|
||||
setOrder(-100);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected LinksHandler getLinksHandler() {
|
||||
return new WebMvcLinksHandler();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void extendInterceptors(List<Object> interceptors) {
|
||||
super.extendInterceptors(interceptors);
|
||||
interceptors.add(0, new MyExtensionSecurityInterceptor());
|
||||
}
|
||||
|
||||
class WebMvcLinksHandler implements LinksHandler {
|
||||
|
||||
@Override
|
||||
@ResponseBody
|
||||
public Map<String, Map<String, Link>> links(HttpServletRequest request, HttpServletResponse response) {
|
||||
return Collections.singletonMap("_links", MyExtensionWebMvcEndpointHandlerMapping.this.linksResolver
|
||||
.resolveLinks(request.getRequestURL().toString()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Actuator extension root web endpoint";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright 2012-2024 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 smoketest.actuator.extension;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication(proxyBeanMethods = false)
|
||||
public class SampleActuatorExtensionApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(SampleActuatorExtensionApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
org.springframework.boot.actuate.autoconfigure.endpoint.condition.EndpointExposureOutcomeContributor=\
|
||||
smoketest.actuator.extension.MyExtensionEndpointExposureOutcomeContributor
|
||||
@@ -0,0 +1,4 @@
|
||||
spring.jmx.enabled=false
|
||||
management.endpoints.web.exposure.exclude=*
|
||||
management.endpoints.myextension.exposure.include=health,beans,configprops
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Copyright 2012-2024 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 smoketest.actuator.extension;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
|
||||
import org.springframework.boot.test.web.client.LocalHostUriTemplateHandler;
|
||||
import org.springframework.boot.test.web.client.TestRestTemplate;
|
||||
import org.springframework.boot.web.client.RestTemplateBuilder;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = { "server.error.include-message=always" })
|
||||
class SampleActuatorExtensionApplicationTests {
|
||||
|
||||
@Autowired
|
||||
private Environment environment;
|
||||
|
||||
@Autowired
|
||||
private TestRestTemplate restTemplate;
|
||||
|
||||
@Autowired
|
||||
private RestTemplateBuilder restTemplateBuilder;
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("rawtypes")
|
||||
void healthActuatorIsNotExposed() {
|
||||
ResponseEntity<Map> entity = this.restTemplate.getForEntity("/actuator/health", Map.class);
|
||||
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("rawtypes")
|
||||
void healthExtensionWithAuthHeaderIsDenied() {
|
||||
ResponseEntity<Map> entity = this.restTemplate.getForEntity("/myextension/health", Map.class);
|
||||
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("rawtypes")
|
||||
void healthExtensionWithAuthHeader() {
|
||||
TestRestTemplate restTemplate = new TestRestTemplate(
|
||||
this.restTemplateBuilder.defaultHeader("Authorization", "Bearer secret"));
|
||||
restTemplate.setUriTemplateHandler(new LocalHostUriTemplateHandler(this.environment));
|
||||
ResponseEntity<Map> entity = restTemplate.getForEntity("/myextension/health", Map.class);
|
||||
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user